C++:将类传递给可变参数函数

4
我正在尝试创建一个类,其行为类似于MS CString(也就是说,将其传递给printf并像指向C字符串的指针一样运行,不需要额外的丑陋黑魔法,如“.c_str()”)。
这是该类的第一个实现,它只是起到作用,但还没有提供任何有用的功能:
#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));
    }
};

我测试的代码:

#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;
}

如果我在类中没有析构函数,那么我可以将它传递给printf,它会像C字符串一样正常工作。但是当我添加了析构函数时,GCC会产生以下错误:
error: cannot pass objects of non-trivially-copyable type 'class CString' through '...'

此外,早期版本的GCC会发出警告 + ud2操作码。

那么问题来了:我是否可以在GCC中使以下结构正常工作,或者是否有任何方法(可能不涉及C varargs),可以使使用方式与上述代码完全相同?


1
我要改变的第一件事是使const char* operator成为常量:const char* operator() const——这可能甚至完全解决了问题,但我不确定。请尝试一下。如果添加一个析构函数,那么这个类就不再是POD(如果没有),这意味着它是非平凡可复制的 - leemes
2
我不知道为什么你想要这种行为。将非平凡类型隐式转换为可变参数列表,正是C ++旨在消除的不良习惯的典型例子。 - paddy
考虑以下情况: CString str1; CString str2 = "text"; str1.Format("%s", str2.GetBuffer());一个类使用第三方转换将自身格式化为自身。多好啊。我不明白的是,为什么他们没有添加像operator...()这样的东西来控制当您通过vararg传递类时实际使用的内容。 - ZZYZX
嗯,是的,说得好。那么,我想你需要自己编写printf和编译器了。(这基本上就是微软所做的事情.. 呵呵)。 - jstine
@paddy所说的。请不要这样做。 - Lightness Races in Orbit
显示剩余3条评论
4个回答

3
你可以使用强制类型转换触发转换运算符:
printf("Test: %s, %s\n", static_cast<const char*>(str), 
       static_cast<const char*>(global_str));

然而,我不知道你是否会遇到任何问题,但在C++代码中避免使用varargs可能是最好的选择。
使用类型安全printf如何呢(来源:维基百科)?
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");
}

我认为libstdc++不支持std::runtime_errorstd::logic_error

这个类有什么优势,比如调用 str.GetBuffer(或像 std::string 一样的 .c_str())?这个类的整个意义在于,写 printf(..., (CString)str) 不应该与写 printf(..., (const char*)str) 有任何区别。 - ZZYZX
@ZZYZX:维基百科上的类型安全printf版本怎么样? - Jesse Good
所以我必须实现自定义printf?不过,这应该只写一次,之后就可以重复使用了。 尽管如此,这个答案到目前为止是最好的。谢谢。 - ZZYZX
是的,答案就是使用可变参数模板。它们提供了与可变函数相同的功能,但以一种安全的方式。 - Jesse Good
这个例子看起来很熟悉...!我们应该在这里注明维基百科吗? - Lightness Races in Orbit

3
你基本上需要调用成员函数,可以直接使用 (foo.c_str()) 或通过转换((char *)foo)等方式进行调用。

否则,它取决于编译器。在 C++03 中,该行为未定义(§5.2.2/7):

当给定参数没有参数时,采用一种方式传递参数,以便接收函数可以通过调用 va_arg(18.7) 获取参数的值。对参数表达式执行左值到右值 (4.1)、数组到指针 (4.2) 和函数到指针 (4.3) 标准转换。在这些转换之后,如果参数没有算术、枚举、指针、成员指针或类类型,则程序无效。如果参数具有非 POD 类型 (第 9 条),则行为未定义。

但在 C++11 中 (§5.2.2/7),它是有条件支持的:

当给定参数没有参数时,采用一种方式传递参数,以便接收函数可以获取参数表达式的值。对参数表达式执行左值到右值 (4.1)、数组到指针 (4.2) 和函数到指针 (4.3) 标准转换。具有 (可能带有 cv 限定符的) std::nullptr_t 类型的参数转换为 void* 类型 (4.10)。在这些转换之后,如果参数没有算术、枚举、指针、成员指针或类类型,则程序无效。传递一个具有非平凡复制构造函数、非平凡移动构造函数或非平凡析构函数的类类型(第 9 条)作为潜在求值的参数,没有相应的参数,是以实现定义语义条件支持的。

“以实现定义语义条件支持”为实现提供了文档说明的支持,但它仍然与未定义的行为差不多。

如果我要这样做,我会设置某种中介使用可变模板。使用此方法,当您传递类型为 std::string 的参数时,将自动将 foo.c_str() 传递给 printf 的重载。这可能需要更多的代码,但至少它实际上可以工作。就个人而言,我会避免整个过程,因为它比值得麻烦。


0

你不能通过可变参数传递对象,只能传递对象指针。但是,你可以使用基于模板的(可变参数)printf 实现,例如 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);
}

如果CString实现正确,这将打印出"Test: string, global string!"。

与Jesse Good的实现不同,这支持标准printf格式说明符

免责声明:我是这个库的作者。


0

你在写这个(说实话有点丑)字符串类的时候,想要解决什么问题呢?为什么不使用其他东西?(比如std::string)在开始编写自己超级优化的字符串之前,请三思而后行...

关于你的问题:你的示例代码真的很幸运!你知道椭圆是如何在C语言的机器码中工作的,以及为什么不允许通过它传递非平凡类型吗?简而言之:printf()只查看格式字符串,如果在其中看到'%s',则假定下一个参数是char*,就这样!因此,如果您传递其他任何内容(例如charshort等),那么它将是未定义行为!(并且在sizeof()与预期不同的情况下,很可能很快就会出现分段错误...这就是为什么椭圆形是C++中的一种不好的实践!它们完全不安全!

如果你在使用C++,千万不要使用 C API!有很多为输出格式设计的C++库(比如boost::format),它们是类型安全的!C++11开启了printf类函数的大门,同时还保证了类型的安全性!只需要阅读一个关于变长模板的“经典”示例... 仅在阅读之后,尝试实现你自己的字符串 :)

  1. 我了解省略号运算符的工作原理,以及目标函数如何从vararg中接收值(至少在VC编译器生成的汇编中 - 从未尝试过反汇编GCC制作的应用程序)。
  2. sizeof(CString) == sizeof(CString :: Buffer),因为它没有任何其他成员或虚拟方法。
- ZZYZX

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接