I'm reading "C++17 The Complete Guide" by Josuttis, and he mentions off-hand in an early chapter that full template specializations must have the same signature and return type because specializations are only implementations, not new declarations. He shows these two examples, which I've simplified. I also checked to make sure they work the same in C++23.
Successfully compiles:
using namespace std::literals;
template<std::size_t> auto func1();
template<> auto func1<0>() { return "1"s; }
template<> auto func1<1>() { return 1; }
Fails to compile:
using namespace std::literals;
template<std::size_t> auto func2();
template<> std::string func2<0>() { return "1"s; }
template<> int func2<1>() { return 1; }
He offers no further explanation or references to other parts of the book.
This confused me; my understanding is that in the successful example, func1<0>
should have its return type deduced to std::string
and func1<1>
to int
. But then, we'd have the same problem as in the second example. So, how does func1
successfully compile? Won't its specializations have different return types?
I happened across this answer to a similar question, but it doesn't clear up my confusion. It provides another example that successfully compiles (modified to match earlier examples):
using namespace std::literals
using types = std::tuple<std::string, int>;
template<std::size_t t> auto func3() -> std::tuple_element_t<t, types>;
template<> std::string func3<0>() { return "1"s; }
template<> int func3<1>() { return 1; }
This makes sense to me, and it shows me a mechanism through which the compiler could be getting func1
to compile: Deduce the return types of the specializations to string
and int
, then deduce the overall template return type to depend on its template parameter with something like tuple_element_t
. However, if the compiler were able to do that second step, I see no reason for it to not be able to use it for func2
as well.
What is happening to cause func1
to successfully compile? Why can't it be done for func2
?
From the C++23 Standard (9.2.9.6 Placeholder type specifiers)
13 If a function or function template F has a declared return type that uses a placeholder type, redeclarations or specializations of F shall use that placeholder type, not a deduced type; otherwise, they shall not use a placeholder type.
And there is an example
template <typename T> auto g(T t) { return t; } // #1
template auto g(int); // OK, return type is int
template char g(char); // error: no matching template
template<> auto g(double); // OK, forward declaration with unknown return type
template <class T> T g(T t) { return t; } // OK, not functionally equivalent to #1
template char g(char); // OK, now there is a matching template
template auto g(float); // still matches #1
Actually if you have two declarations like
auto f();
int f();
then the second declaration redeclares the first function declaration with different return type specifier. Placeholders are type specifiers .
Don't mix trailing return types with return types with placeholders.