转换操作符模板专门化

Conversion operator template specialization

本文关键字:专门化 操作符 转换      更新时间:2023-10-16

这是一个理解转换操作符、模板和模板专门化的主要学术练习。下面代码中的转换操作符模板适用于int, floatdouble,但不适用于std::string…排序的。我已经创建了一个转换为std::string的专门化,它在与初始化std::string s = a;一起使用时有效,但在与强制转换static_cast<std::string>(a)一起使用时失败。

#include <iostream>
#include <string>
#include <sstream>
class MyClass {
     int y;
public:
    MyClass(int v) : y(v) {}
    template <typename T>
    operator T() { return y; };
};
template<>
MyClass::operator std::string() {
    std::stringstream ss;
    ss << y << " bottles of beer.";
    return ss.str();
}
int main () {
    MyClass a(99);
    int i    = a;
    float f  = a;
    double d = a;
    std::string s = a;
    std::cerr << static_cast<int>(a) << std::endl;
    std::cerr << static_cast<float>(a) << std::endl;
    std::cerr << static_cast<double>(a) << std::endl;
    std::cerr << static_cast<std::string>(a) << std::endl; // Compiler error
}

以上代码在g++和icc中产生一个编译错误,两者都抱怨没有用户定义的转换适合将MyClass实例转换为static_cast上的std::string (c风格的强制转换行为相同)。

如果我用显式的、非模板版本的转换操作符替换上面的代码,一切都很好:

class MyClass {
    int y;
public:
    MyClass(int v) : y(v) {}
    operator double() {return y;}
    operator float()  {return y;}
    operator int()    {return y;}
    operator std::string() {
        std::stringstream ss;
        ss << y << " bottles of beer.";
        return ss.str();
    }
};

我的模板专门化std::string有什么问题?为什么它可以用于初始化而不能用于类型转换?

更新:

经过@luc-danton的一些模板魔法(我以前从未见过的元编程技巧),在启用实验性c++ 0x扩展后,我在g++ 4.4.5中运行了以下代码。除了这里所做的事情的恐怖之外,需要实验性编译器选项本身就足以说明这样做。无论如何,希望这篇文章对其他人和我一样有教育意义:

class MyClass {
    int y;
public:
    MyClass(int v) : y(v) {}
    operator std::string() { return "nobody"; }
    template <
        typename T
        , typename Decayed = typename std::decay<T>::type
        , typename NotUsed = typename std::enable_if<
            !std::is_same<const char*, Decayed>::value &&
            !std::is_same<std::allocator<char>, Decayed>::value &&
            !std::is_same<std::initializer_list<char>, Decayed>::value
          >::type
    >
    operator T() { return y; }
};

这显然迫使编译器选择将std::string转换为operator std::string(),从而避免了编译器遇到的歧义。

只要使用

就可以重现这个问题
std::string t(a);

结合GCC (error: call of overloaded 'basic_string(MyClass&)' is ambiguous)的实际错误,我们对可能发生的事情有了强有力的线索:在复制初始化 (std::string s = a;)的情况下有一个首选转换序列,而在直接初始化的情况下(std::string t(a);static_cast)至少有两个序列,其中一个不能优先于另一个。

查看所有接受一个参数的std::basic_string显式构造函数(在直接初始化而不是复制初始化期间只考虑这些构造函数),我们发现explicit basic_string(const Allocator& a = Allocator());实际上是唯一的显式构造函数。

不幸的是,除了诊断之外,我不能做太多:我想不出一个技巧来发现operator std::allocator<char>是否被实例化(我尝试过SFINAE和operator std::allocator<char>() = delete;,没有成功),我对函数模板专门化,重载解析和库要求了解得太少,无法知道GCC的行为是否符合。

既然你说这个练习是学术性的,我就不像你那样批评非显式转换操作符不是一个好主意了。我认为你的代码是一个足够好的例子,为什么无论如何:)


我让SFINAE工作。如果将操作符声明为:

template <
    typename T
    , typename Decayed = typename std::decay<T>::type
    , typename = typename std::enable_if<
        !std::is_same<
            const char*
            , Decayed
        >::value
        && !std::is_same<
            std::allocator<char>
            , Decayed
        >::value
        && !std::is_same<
            std::initializer_list<char>
            , Decayed
        >::value
    >::type
>
operator T();

然后没有歧义,代码将被编译,std::string的专门化将被选中,结果程序将按照期望的方式运行。我仍然没有解释为什么复制初始化是好的

这里的static_cast等同于std::string(a)

注意std::string s = std::string(a);也不能编译。我的猜测是,构造函数有很多重载,模板版本可以将a转换为许多合适的类型。

另一方面,对于固定的转换列表,其中只有一个与字符串的构造函数接受的类型完全匹配。

要测试这一点,添加对const char*的转换-非模板化版本应该在相同的地方开始失败。

现在的问题是为什么std::string s = a;工作。这和std::string s = std::string(a);之间的细微差别只有神知道