ZZYZX
ZZYZX

Reputation: 45

C++: Passing classes to vararg function

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?

Upvotes: 4

Views: 10910

Answers (4)

vitaut
vitaut

Reputation: 55605

You can't pass objects via varargs, only pointers to objects. However, you can use a (variadic) template-based implementation of printf such as the one provided by C++ Format:

#include "format.h"
#include "CString.hpp"

CString global_str = "global string!";

std::ostream &operator<<(std::ostream &os, const CString &s) {
  return os << static_cast<const char*>(s);
}

int main() {
  CString str = "string";
  fmt::printf("Test: %s, %s\n", str, global_str);
}

This will print "Test: string, global string!" provided that CString is implemented correctly.

Unlike Jesse Good's implementation, this supports standard printf format specifiers.

Disclaimer: I'm the author of this library

Upvotes: 0

Jesse Good
Jesse Good

Reputation: 52365

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.

Upvotes: 3

zaufi
zaufi

Reputation: 7129

what are you trying to solve writing this (ugly, to be honest) string class?? why not to use smth else? (like std::string) -- please think twice before starting to write your-own-super-optimized strings...

about your question: you are really lucky with your sample code! have you any idea how ellipse works (in machine code) in C and why it is not allowed to pass non-trivial types via it? In brief: printf() just looks into a format string and if it sees '%s' in it it assumes that next argument is a char* thats all! So if you pass anything else (like char, short, etc) instead -- it will be UB! (and in case of different sizeof() than expected it is high probability that you'll get a segmentation fault soon... It is why ellipses is a bad practice in C++! they are completely type-unsafe!

If you are using C++, just do not use C API! There are plenty C++ libs designed for formatting output (like boost::format) and they are type-safe! C++11 open the door for printf-like function but with type safe guarantee! just read a "classic" example about variadic templates... and only after reading, try to implement your own strings :)

Upvotes: 0

Jerry Coffin
Jerry Coffin

Reputation: 490348

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.

Upvotes: 3

Related Questions