【问题标题】:Conversion operator template specialization转换运算符模板特化
【发布时间】:2011-12-06 04:36:56
【问题描述】:

这是了解转换运算符、模板和模板专业化的主要学术练习。以下代码中的转换运算符模板适用于 intfloatdouble,但与 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 实例转换为 std::string 上的 static_cast (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(),从而克服编译器遇到的任何歧义。

【问题讨论】:

  • 可能相关,但我不确定具体如何:gotw.ca/publications/mill17.htm
  • @sth:链接的文章讨论了模板专业化和解析顺序。为了查看这是否是我遇到的问题(似乎合理),我删除了我对 std::string 的专业化。不幸的是,转换导致 same 编译器错误。如果这是一个不同的错误,我会说它可能是相关的。唉,看来不是这样。
  • 这就是文章的重点:特化有助于解决重载问题。

标签: c++ templates metaprogramming template-specialization


【解决方案1】:

您只需使用即可重现问题

std::string t(a);

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

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

不幸的是,除了诊断之外我无能为力:我想不出一个技巧来发现 operator std::allocator&lt;char&gt; 是否已实例化(我尝试了 SFINAE 和 operator std::allocator&lt;char&gt;() = 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 的特化将被选中,生成的程序将按预期运行。我仍然没有解释为什么复制初始化很好。

【讨论】:

  • 为什么复制初始化是明确定义的呢? std::string 还具有来自 const char * 的非显式单参数构造函数——复制构造函数是否优于转换构造函数?
  • @KerrekSB 正如我所说,我对重载解决方案知之甚少。我只关注std::string s = a;std::string t(a); 之间的差异。 (GCC 的错误消息也非常明确地说明了直接初始化情况下发生的情况。)检查规则——我希望对于从 U 到 T 的转换,U -&gt; T 通过U::operator T 优先于U -&gt; const char* -&gt; T (通过U::operator const char* 然后T(const char*))或类似的。
  • 使用非模板化版本,cerr &lt;&lt; std::string(a) &lt;&lt; endl; 在 g++ 和 icc 中同样有效。这似乎是最难破译的语法,但编译器却乐于接受它。我以前追过你在此处列出的相同路径,包括使用 std::string 类型到std::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt; 的显式扩展,但从未到达任何地方。让编译器感到困惑的是我无法理解(显然编译器也是如此)。
  • @Nathan 第二种情况(没有转换运算符模板)可能会起作用,因为不使运算符成为模板可能会使它成为首选。在第一种情况下,专门 operator T 不会帮助解决重载问题(我建议在您的 cmets 中使用 sth 的链接)。换句话说,仅仅因为您可以提供与编写模板(包括任何或没有特化)相同的具有多个重载的 功能 并不意味着语言规则是相同的。
  • @Nathan: std::string(a) 始终有效,因为它显式调用转换运算符。如果你愿意,你可以说cout &lt;&lt; string(a)。但是string b(a); 不起作用(而string b(string(a)); 起作用)。
【解决方案2】:

static_cast 这里相当于std::string(a)

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

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

要对此进行测试,请将转换添加到 const char* - 非模板版本应该在同一个地方开始失败。

(现在的问题是为什么std::string s = a; 有效。它和std::string s = std::string(a); 之间的细微差别只有上帝知道。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-10-14
    • 1970-01-01
    • 1970-01-01
    • 2021-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-05
    相关资源
    最近更新 更多