Overloading of C++ templated functions

2019-04-29 07:57发布

I would think that the following code should be working, but both g++ and clang++ return the exact same error (although Visual C++ 2012 doesn't).

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

The error is due to the last line and the fact that the second and third result functions are considered equivalent. Although I would think the second one is a better fit than the third one (like the first one is).

I am not sure though. Can anybody tell me if I am wrong or if the compiler is?

3条回答
男人必须洒脱
2楼-- · 2019-04-29 08:27

TLDR; The reason that your program fails to compile is that the second and third overload are equally good matches during overload resolution. In particular, neither is more specialized than the other. Because overload resolution cannot select a best match, the program is ill-formed. The cure is to SFINAE your way out of it.

The problem

14.5.6.2 Partial ordering of function templates [temp.func.order]

2 Partial ordering selects which of two function templates is more specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type. The deduction process determines whether one of the templates is more specialized than the other. If so, the more specialized template is the one chosen by the partial ordering process.

3 To produce the transformed template, for each type, non-type, or template template parameter (including template parameter packs (14.5.3) thereof) synthesize a unique type, value, or class template respectively and substitute it for each occurrence of that parameter in the function type of the template.

For all three overloads, the first synthesized argument is equal, and since all arguments are considered one by one, we can focus on the second one.

Your first overload is transformed to the following synthesized 2nd argument

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

Your second overload is transformed to the following synthesized 2nd argument

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

Your third overload is transformed to the following synthesized 2nd argument

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

14.8.2.4 Deducing template arguments during partial ordering [temp.deduct.partial]

2 Two sets of types are used to determine the partial ordering. For each of the templates involved there is the original function type and the transformed function type. [ Note: The creation of the transformed type is described in 14.5.6.2. — end note ] The deduction process uses the transformed type as the argument template and the original type of the other template as the parameter template. This process is done twice for each type involved in the partial ordering comparison: once using the transformed template-1 as the argument template and template-2 as the parameter template and again using the transformed template-2 as the argument template and template-1 as the parameter template.

It is clear that the first and second overload have no 2nd template parameter to deduce and so they are at least as specialized as the third overload. The question is whether the third can have it's N parameter deduced from the first and second overloads' synthesized 2nd argument.

For the first overload, this is true for N=0, and so the first overload is more specialized than the third. This is why your first function call selects the first overload.

For the third overload, argument deduction does not take place it is a non-deduced context:

14.8.2.5 Deducing template arguments from a type [temp.deduct.type]

5 The non-deduced contexts are:

— ...

A non-type template argument or an array bound in which a subexpression references a template parameter.

— ...

This means that the third overload is also at least as specialized as the second one. Hence, overload resolution is not able to select one, and the program is ill-formed.

The cure

Simply make two overloads with a non-overlapping condition inside an enable_if (using SFINAE). This bypasses overload resolution in this case.

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

Live Example.

查看更多
霸刀☆藐视天下
3楼-- · 2019-04-29 08:37

You should replace your overloads with some tag dispatching.

Write one function, then check if the second arg A is_same as the first type in the tuple in a static way, calling another function with a type dependent on that. Repeat for last on the false branch.

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

with maybe some decay or remove_const in there.

The idea is that std::is_same<X,Y> is true_type if they are the same, and false_type otherwise. helper overloads the third argument on both true and false type, giving you a compile time branch. Repeat again for the last type, and you are done.

查看更多
聊天终结者
4楼-- · 2019-04-29 08:42

In the second overload, the std::tuple_size<Tuple>::value-1 part depends on the template parameter Tuple and is hence not a better match or, in C++ speak, "more specialized". This is why it is considered equal wrt overloading with the third one which explicitly has N.

Only your first overload uses a constant value of 0 which is not dependent on Tuple and is therefore a better match.


In case you want to solve your problem, you can disable the third overload for when it would match the second one:

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;
}
查看更多
登录 后发表回答