Understanding when a default template parameter is a redefinition and when not in SFINAE technique
Asked Answered
S

1

7

An SFINAE technique is to to use a default class to enable/disable a function. However this doesn't work with overloading functions, resulting in "template parameter redefines default argument":

template <class T, class = std::enable_if_t<std::is_integral_v<T>>>
auto foo(T) { return 1; }

template <class T, class = std::enable_if_t<std::is_floating_point_v<T>>>
auto foo(T) { return 2; }

// error template parameter redefines default argument"

The common solution is to use default non-type template parameter:

template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
auto foo(T) { return 2; }

// works

This works only when the condition is "different". But understanding when the condition is "different" is not that simple.

Start with the most obvious example (identical (token by token) conditions):

template <class T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
auto foo(T) { return 2; }

// error template parameter redefines default argument"

With two different non-dependent conditions, that evaluate to the same value:

constexpr bool true_1 = true;
constexpr bool true_2 = true;

template <class T, std::enable_if_t<true_1, T> = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<true_2, T> = 0>
auto foo(T) { return 2; }

// error template parameter redefines default argument"

With two different dependant conditions, that evaluate to the same value:

template <class T> constexpr bool true_1 = true;
template <class T> constexpr bool true_2 = true;

template <class T, std::enable_if_t<true_1<T>, T> enable = 0>
auto foo(T) { return 1; }

template <class T, std::enable_if_t<true_2<T>, T> enable = 0>
auto foo(T) { return 2; }

// works
// (of course will give ambiguous call error when trying to call it,
//  but the point here is you are allowed to declare them like this)

In this last example if I call foo (i.e. foo(24)) both overloads have the exact same parameters and template parameters:

error: call to 'foo' is ambiguous

return foo(12);
       ^~~

note: candidate function [with T = int, enable = 0]

auto foo(T) { return 1; }
     ^

note: candidate function [with T = int, enable = 0]

auto foo(T) { return 2; }
     ^

This seems to effectively instantiates two identical overloads (in terms of declaration, not definition).

All of my question are very closely related so I ask all of them here:

  1. What are the exact rules when two non-type template arguments are not considered a redefinition?
  2. How does the standard deal with two declaration-identical overloads (like in the last example)
  3. Why does this work for non-type template arguments, but not for type template arguments?
Septivalent answered 24/1, 2021 at 12:27 Comment(3)
eel.is/c++draft/temp.over.link#5.sentence-1Canady
"All of my question are very closely related so I ask all of them here:" - Even if your questions are related, more than one question per post is generally recommended against (ruled against?), but more importantly I think it will make answerers (myself included) to abstain from even starting to answer the question (it's just too big). In this case, particularly, none of the three questions is trivial, and could each be eligible for their separate (follow-up) questions.Permute
Is this related?Carpentaria
A
2

To get things started:

// #1 Two templates with default type parameters.
template <typename T, typename T2 = int>
void Foo(T) {}
template <typename T, typename T2 = char>   // Error – redefinition.
void Foo(T) {}

// #2 Two templates with default non-type parameters.
template <typename T, int = 0>
void Foo(T) {}
template <typename T, char = 'x'>          // Allowed, but ambiguous for Foo<type>(value).
void Foo(T) {}

Why it doesn't work with type parameters?

#1 and #2 are not equivalent. In the first two templates parameters T2 are similar (allowing any type), they are just defaulted with different value (type).

Furthermore standard states that:

The set of default template-arguments available for use is obtained by merging the default arguments from all prior declarations of the template in the same way default function arguments are (11.3.6). Example:

template<class T1, class T2 = int> class A;
template<class T1 = int, class T2> class A;

is equivalent to

template<class T1 = int, class T2 = int> class A;

Multiple defaulted types at the same positions cannot coexist.

Why does it work with non-type parameters?

With non-type parameters you essentially create separate templates with concrete types (something like specializations), which from the start creates separate signature for those functions.

Shining some light on your example

constexpr bool true_1 = true;
constexpr bool true_2 = true;
// Shorter version of second parameter, nameless.
template <class T, std::enable_if_t<true_1>* = 0> auto foo(T) { return 1; }
template <class T, std::enable_if_t<true_2>* = 0> auto foo(T) { return 2; }

This didn't work because both constants are always effectively the same "true", yielding the same type for non-type parameter.

template <class T> constexpr bool true_1 = true;
template <class T> constexpr bool true_2 = true;
template <class T, std::enable_if_t<true_1<T>>* = 0> auto foo(T) { return 1; }
template <class T, std::enable_if_t<true_2<T>>* = 0> auto foo(T) { return 2; }

This example provides uncertainty factor - T. Compiler cannot know what it will instantiate it with when it sees only declarations of your foo<> functions. There might be specialization for true_2 that will point to false. Hence true_1 and true_2 are treated as different types.

Arabella answered 24/5, 2023 at 3:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.