【问题标题】:Unexpected implicit cast when using variadic templates?使用可变参数模板时出现意外的隐式转换?
【发布时间】:2014-02-23 03:02:48
【问题描述】:

鉴于此代码:

#include <iostream>

template<typename... Args> class X;

template<typename T>
class X<T> {
 public:
    T value;
    X(T value_) : value(value_) {}
};

template<typename T, typename... Args>
class X<T, Args...> : public X<Args...> {
 public:
    T value;
    X(T value_, Args... args) : value(value_), X<Args...>(args...) {}
};

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {
    stream << value_.value;
    return stream;
}

template<typename T, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, Args...>& value_) {
    stream << value_.value << " " << static_cast<X<Args...> const& >(value_);
    return stream;
}

class Person : public X<std::string, int>{
 public:
    Person(std::string name, int age) : X<std::string, int>(name, age) {}
};


int main()
{
   std::cout << Person("Me", 35) << std::endl;
   return 0;
}

使用 'g++ -std=c++11 main.cpp' 使用 gcc 4.7.2 编译,执行时给出以下输出:

35

我期待它打印出来:

Me 35

因为它应该首先匹配 Person 的直接超类而不是其超超类的 operator 行为的 int,而不是作为直接超类 X<:string int> 行为的两个项目。这是预期的吗?

【问题讨论】:

  • 我怀疑和clang++ agrees&lt;&lt; static_cast&lt;X&lt;Args...&gt; &gt;(value_) 中的运算符&lt;&lt; 模棱两可:两个函数模板都是可行的,具有相同的等级,据我所知,两者都不是更专业的(尽管有一个额外的参数包!)。请参阅 [temp.func.order]/5 中的注释
  • 规范性参考:[temp.deduct.partial]/3 “用于确定排序的类型取决于完成部分排序的上下文:- 在函数调用的上下文中,使用的类型是那些函数调用有参数的函数参数类型。" [强调我的]
  • 这似乎是 gcc 的一个错误。 VC++ 2013 CTP 可以如您预期的那样产生正确的结果。例如输出是“我 35”
  • @xmllmx 太好了。现在我们有了三个编译器和三种不同的行为。要么在标准中未指定(或未定义),要么其中至少有两个存在错误。

标签: c++ c++11 casting variadic-templates implicit-conversion


【解决方案1】:

解决您的问题:

#include <iostream>

template<typename... Args> class X;

template<typename T>
class X<T> {
 public:
    T value;
    explicit X(T value_) : value(value_) {}
};

template<typename T, typename... Args>
class X<T, Args...> : public X<Args...> {
 public:
    T value;
    explicit X(T value_, Args... args) : X<Args...>(args...), value(value_) {}
};

这种类型让我们可以使用标签调度来根据X的参数数量选择一个重载:

template<std::size_t> struct compile_time_size {};

这里有 3 个重载。第一个错误在 0 个参数上,第二个处理 1,第三个处理 N

template<typename... Ts>
std::ostream& output_helper(std::ostream&, const X<Ts...>&, compile_time_size<0>) = delete;

template<typename T, typename... Ts>
std::ostream& output_helper(std::ostream& stream, const X<T, Ts...>& value, compile_time_size<1>)
{
    stream << value.value;
    return stream;
}
template<typename T, typename... Ts, std::size_t N>
std::ostream& output_helper(std::ostream& stream, const X<T, Ts...>& value, compile_time_size<N>)
{
    stream << value.value << " " << static_cast< X<Ts...> const& >(value);
    return stream;
}

我们的&lt;&lt; 现在只是一个重载,这使得推理变得容易。然后它分派给上述辅助函数,显式传递长度。他们使用 ADL 重新调度回&lt;&lt; 以查找在他们之后声明的函数:

template<typename... Ts>
std::ostream& operator<<(std::ostream& stream, const X<Ts...>& value) {
    return output_helper( stream, value, compile_time_size<sizeof...(Ts)>() );
}

live example

【讨论】:

  • “如果可能的话,函数更喜欢 matxh 没有 ...。” 我不太明白你的意思。您的解决方案应该可以工作,因为它通过使两个函数模板对某些数量的模板参数相互独占,从而消除了两个函数模板之间的歧义。
  • 正如我在对 OP 的 cmets 中提到的,coliru 的 clang++3.5 认为通过 &lt;&lt; static_cast&lt;X&lt;Args...&gt; &gt;(value_) 的调用是模棱两可的。 IE。两者都不是更专业,空参数包似乎与重载解析无关。
  • @dyp clang 的重载解析代码似乎忽略了空参数包:我不确定它们是否正确。我基于我的信念,即空包将重载选择降级到其他匹配的重载之下。不过,我暂时不会使用真正的计算机。
  • 所以,在我看来,这个解决方案并不是真正的通用,如果 Person 扩展 X<:string> 会怎样?难道我们不会再次回到同样的问题吗?
  • @PeteFordham 你应该有两个重载:一个只需要 1 个参数,另一个需要 2 个或更多。应删除取 1 或更多的过载。顺便说一句,演员表应该是转换为const-reference,而不是转换为值。我会解决的。
【解决方案2】:

发布的代码有两个问题。首先是之间的歧义:

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {

和:

template<typename T, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, Args...>& value_) {

在参数包为空的情况下。这可以通过改用这两个函数来解决:

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {

和:

template<typename T, typename T2, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, T2, Args...>& value_) {

现在模板匹配的歧义得到了解决。

这给我们留下了第二个问题。这一行:

  std::cout << Person("Me", 35) << std::endl;

Person 没有提供 operatorX< std::string, int > 和 X&lt; int &gt; 都有。但是,编译器现在可以在这两者之间进行选择,因为它们都是 Person 的超类。

这可以通过两种方式解决,首先是添加一个特定的操作符

std::ostream& operator <<(std::ostream& stream, const Person &value_) {
   stream << static_cast<const X<std::string, int, int>&>(value_);
   return stream;
}

效果很好,但我发现它过于冗长。或者,我们可以通过删除类层次结构并仅使用成员变量来包含“其他”值来消除歧义:

template<typename T>
class X<T> {
public:
    T value;
    X(T value_) : value(value_) {}
};

template<typename T>
std::ostream& operator <<(std::ostream& stream, const X<T>& value_) {
   stream << value_.value << ".";
   return stream;
}


// Empty arg packs will match ambiguously with nothing so make sure the empty pack case is distanct from the base case

template<typename T, typename T2, typename... Args>
class X<T, T2, Args...> {
public:
   T value;
   X<T2, Args...> superValue;
   X(T value_, T2 arg, Args... args) : value(value_), superValue(arg, args...) {}
};

template<typename T, typename T2, typename... Args>
std::ostream& operator <<(std::ostream& stream, const X<T, T2, Args...>& value_) {
   stream << value_.value << "," << value_.superValue;
   return stream;
}

现在 Person 只能被隐式转换为 X &lt; std::string,int &gt; 而没有别的,也没有歧义。最近的 g++ 和 clang 都编译这个没有问题。

至于在模板解析中是否没有任何东西应该匹配空的 arg 包,或者在转换解析中是否应该匹配更直接的基类以支持不那么直接的基类,我留给比我自己更精通 c++ 标准的人。

【讨论】:

  • 我给了它一个 +1,它的作品很值得。并且不需要我做的反弹。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-04-21
  • 2016-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-13
相关资源
最近更新 更多