5

我认为以下代码应该可以工作,但是 g++ 和 clang++ 都返回完全相同的错误(尽管 Visual C++ 2012 没有)。

#include <iostream>
#include <tuple>

template <int N, typename T>
struct A { };

template <typename Tuple>
double result(const Tuple& t, const A<0, typename std::tuple_element<0, Tuple>::type>& a)
{
  return 0;
}

template <typename Tuple>
double result(const Tuple& t, const A<std::tuple_size<Tuple>::value-1,
                                      typename std::tuple_element<std::tuple_size<Tuple>::value-1,Tuple>::type>& a)
{
  return 1;
}

template <typename Tuple, int N>
double result(const Tuple& t, const A<N, typename std::tuple_element<N, Tuple>::type>& a)
{
  return 0.5;
}

int main()
{
  auto a = std::make_tuple(0, 1, 2., 3., 4);
  std::cout << result(a, A<0,int>()) << std::endl;
  std::cout << result(a, A<2,double>()) << std::endl;
  std::cout << result(a, A<4,int>()) << std::endl; // Fails if uncommented
  return 0;
}

错误是由于最后一行以及第二个和第三个result函数被认为是等效的事实。虽然我认为第二个比第三个更合适(就像第一个一样)。

不过我不确定。谁能告诉我我错了还是编译器错了?

4

3 回答 3

6

TLDR;您的程序无法编译的原因是第二个和第三个重载在重载解析期间同样匹配。特别是,两者都不比另一个更专业。因为重载决议不能选择最佳匹配,所以程序是非良构的。解决方法是让 SFINAE 摆脱困境。

问题

14.5.6.2 函数模板的部分排序[temp.func.order]

2 部分排序通过依次转换每个模板(参见下一段)并使用函数类型执行模板参数推导来选择两个函数模板中的哪一个比另一个更专业。推演过程确定模板中的一个是否比另一个更专业。如果是这样,更专业的模板是部分排序过程选择的模板。

3 要生成转换后的模板,对于每个类型、非类型或模板模板参数(包括其模板参数包 (14.5.3)),分别合成一个唯一的类型、值或类模板,并将其替换为该模板的每次出现模板的函数类型中的参数。

对于所有三个重载,第一个综合参数是相等的,并且由于所有参数都被一个一个地考虑,我们可以专注于第二个。

您的第一个重载转换为以下合成的第二个参数

const A<0, typename std::tuple_element<0, Arg1>::type>&

您的第二个重载将转换为以下合成的第二个参数

const A<
        std::tuple_size<Arg1>::value-1, typename        
        std::tuple_element<std::tuple_size<Arg1>::value-1, Arg1>::type
>&

您的第三个重载转换为以下合成的第二个参数

const A<Arg2, typename std::tuple_element<Arg2, Arg1>::type>&    

14.8.2.4 在部分排序期间推导模板参数 [temp.deduct.partial]

2 两组类型用于确定偏序。对于涉及的每个模板,都有原始函数类型和转换后的函数类型。[ 注意:转换类型的创建在 14.5.6.2 中描述。— end note ] 推演过程使用转换后的类型作为参数模板,将另一个模板的原始类型作为参数模板。对于偏序比较中涉及的每种类型,此过程执行两次:一次使用转换后的模板 1 作为参数模板,模板 2 作为参数模板,再次使用转换后的模板 2 作为参数模板和模板 1作为参数模板。

很明显,第一个和第二个重载没有要推导的第二个模板参数,因此它们至少与第三个重载一样专门。问题是第三个是否可以N从第一个和第二个重载的综合第二个参数中推导出它的参数。

对于第一个重载,这对于 是正确的N=0,因此第一个重载比第三个更专业。这就是您的第一个函数调用选择第一个重载的原因。

对于第三个重载,参数推导不会发生,它是一个非推导上下文:

14.8.2.5 从类型推导模板参数 [temp.deduct.type]

5 未推断的上下文是:

— ...

非类型模板参数或绑定的数组,其中子表达式引用模板参数。

— ...

这意味着第三个重载也至少与第二个重载一样专业。因此,重载决议无法选择一个,并且程序格式错误。

治愈

enable_if只需在(使用 SFINAE)内部创建两个具有非重叠条件的重载。在这种情况下,这绕过了重载解决方案。

template <typename Tuple, int N>
typename std::enable_if<N == std::tuple_size<Tuple>::value-1, double>::type
result(const Tuple& t, const A<N, typename std::tuple_element<N, Tuple>::type>& a)
{
  return 1;
}

template <typename Tuple, int N>
typename std::enable_if<N != std::tuple_size<Tuple>::value-1, double>::type
result(const Tuple& t, const A<N, typename std::tuple_element<N, Tuple>::type>& a)
{
  return 0.5;
}

活生生的例子

于 2013-10-16T14:45:29.163 回答
6

在第二个重载中,该std::tuple_size<Tuple>::value-1部分取决于模板参数Tuple,因此不是更好的匹配,或者用 C++ 来说,“更专业”。这就是为什么它被认为与显式具有的第三个重载相同的原因N

只有您的第一个重载使用0不依赖于的常量值,Tuple因此是更好的匹配。


如果你想解决你的问题,你可以禁用第三个重载,因为它会匹配第二个:

template <typename Tuple, int N>
typename std::enable_if< N != std::tuple_size<Tuple>::value-1, double >::type
result(const Tuple& t, const A<N, typename std::tuple_element<N, Tuple>::type>& a)
{
  return 0.5;
}
于 2013-10-14T17:21:27.550 回答
3

你应该用一些标签调度替换你的重载。

编写一个函数,然后A is_same以静态方式检查第二个 arg 是否作为元组中的第一个类型,调用另一个类型依赖于该类型的函数。在假分支上重复最后一个。

 helper( t, a, std::is_same<A, std::tuple_element<0, Tuple>>() );

也许有一些decayremove_const在那里。

这个想法是,std::is_same<X,Y>如果true_type它们相同,false_type否则。 helper重载 true 和 false 类型的第三个参数,为您提供编译时分支。对最后一种类型再次重复,您就完成了。

于 2013-10-14T17:46:41.320 回答