C++: Passing classes to vararg function

10,015

Solution 1

You can trigger the conversion operator with a cast:

printf("Test: %s, %s\n", static_cast<const char*>(str), 
       static_cast<const char*>(global_str));

However, I don't know if you will run into any problems with this, avoiding varargs in C++ code would probably be the best.

How about using the type-safe printf instead (Credit: Wikipedia):

void printf(const char *s)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                throw std::runtime_error("invalid format string: missing arguments");
            }
        }
        std::cout << *s++;
    }
}

template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                std::cout << value;
                printf(s + 1, args...); // call even when *s == 0 to detect extra arguments
                return;
            }
        }
        std::cout << *s++;
    }
    throw std::logic_error("extra arguments provided to printf");
}

I don't think libstdc++ supports std::runtime_error and std::logic_error though.

Solution 2

You pretty much need to invoke a member function, either directly (foo.c_str()) or via something like a cast ((char *)foo).

Otherwise, it depends on the compiler. In C++03, the behavior is undefined (§5.2.2/7):

When there is no parameter for a given argument, the argument is passed in such a way that the receiving function can obtain the value of the argument by invoking va_arg (18.7). The lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions are performed on the argument expression. After these conversions, if the argument does not have arithmetic, enumeration, pointer, pointer to member, or class type, the program is ill-formed. If the argument has a non-POD class type (clause 9), the behavior is undefined.

...but in (C++11, §5.2.2/7), it's conditionally supported:

When there is no parameter for a given argument, the argument is passed in such a way that the receiving The lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions are performed on the argument expression. An argument that has (possibly cv-qualified) type std::nullptr_t is converted to type void* (4.10). After these conversions, if the argument does not have arithmetic, enumeration, pointer, pointer to member, or class type, the program is ill-formed. Passing a potentially-evaluated argument of class type (Clause 9) having a nontrivial copy constructor, a non-trivial move constructor, or a non-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics.

"conditionally-supported with implementation-defined semantics" leaves an opening for an implementation to support it with proper documentation, but it's still about as close to undefined behavior as you can get.

If I were going to do this, I think I'd set up some sort of intermediary using a variadic template. With this, you'd supply an overload that (for example) automatically passed foo.c_str() to printf when you passed an argument of type std::string. It's (probably) more code, but at least it'll actually work. Personally, I'd avoid the whole thing as simply being more trouble than it's worth though.

Share:
10,015
ZZYZX
Author by

ZZYZX

Updated on August 21, 2022

Comments

  • ZZYZX
    ZZYZX over 1 year

    I'm trying to make a class that behaves like MS CString (that is, you pass it to printf and it acts just like a pointer to C string, without additional ugly black magic like ".c_str()").

    This is very first implementation of this class, that just works and doesn't yet provide anything useful:

    #include <cstdlib>
    #include <cstring>
    
    class CString
    {
    protected:
        struct CStringInfo
        {
            size_t Length;
            size_t MaxLength;
        };
    
    public:
        CString()
        {
            Buffer = NULL;
    
            Assign(NULL);
        }
    
        CString(const char* chv)
        {
            Buffer = NULL;
    
            Assign(chv, 0);
        }
    
        ~CString()
        {
            if(Buffer) delete[] Buffer;
            Buffer = NULL;
        }
    
        size_t GetLength()
        {
            if(!Buffer) Alloc(1);
            return GetInfo()->Length;
        }
    
        size_t Resize(size_t size)
        {
            Alloc(size + 1); // + 0x00
            Buffer[size] = 0;
            return size;
        }
    
        bool Assign(const char* value, size_t size = 0)
        {
            size_t strl = ((size) ? size : strlen(value));
    
            if(!value || !(strl = strlen(value)))
            {
                if(!Buffer) Alloc(1);
                return false;
            }
    
            Alloc(strl + 1);
            memcpy(Buffer, value, strl);
            Buffer[strl] = 0;
            return true;
        }
    
        CString& operator = (const char* what)
        {
            Assign(what);
            return (*this);
        }
    
        CString& operator = (CString& string)
        {
            Assign(string.Buffer);
            return (*this);
        }
    
        operator const char* ()
        {
            return Buffer;
        }
    
    protected:
        char* Buffer;
    
        void Alloc(size_t size)
        {
            if(!size) size = 1;
            char* nb = new char[size + sizeof(CStringInfo)];
            char* nbb = nb + sizeof(CStringInfo);
            size_t cl = size - 1;
            if(Buffer)
            {
                if(cl > GetInfo()->Length) cl = GetInfo()->Length;
                if(cl) memcpy(nbb, Buffer, cl - 1);
                nbb[cl] = 0;
                *(CStringInfo*)(nb) = *(CStringInfo*)(Buffer);
                delete[] (Buffer - sizeof(CStringInfo));
            }
    
            Buffer = nb;
            GetInfo()->MaxLength = size;
            GetInfo()->Length = cl;
        }
    
        void Free()
        {
            if(Buffer)
            {
                delete[] (Buffer - sizeof(CStringInfo));
            }
        }
    
        CStringInfo* GetInfo()
        {
            return (CStringInfo*)(this->Buffer - sizeof(CStringInfo));
        }
    };
    

    And code I test it on:

    #include <cstdio>
    #include "CString.hpp"
    
    CString global_str = "global string!";
    
    int main(int argc, char* argv[])
    {
        CString str = "string";
        printf("Test: %s, %s\n", str, global_str);
        return 0;
    }
    

    If I don't have a destructor in the class, then I can pass it to printf and it will work just like it should (as a C string). But when I add destructor, GCC produces following error:

    error: cannot pass objects of non-trivially-copyable type 'class CString' through '...'
    

    And in addition to that prior versions of GCC will give a warning + ud2 opcode.

    So... Question: can I actually make following construction work in GCC or is there any way, possibly not involving C varargs, to make something identical in use to above code?