Why does enable_if<>* = nullptr work when enable_if<> = void doesn't?
Asked Answered
G

1

7

Basic Problem Statement

I'm learning about SFINAE. I tried an extremely simple enable_if:

// 1: A foo() that accepts arguments that are derived from Base
template <typename T, typename Enable = enable_if_t<std::is_base_of_v<Base, T>>>
void foo(T thing) {
    std::cout << "It's a derived!" << std::endl;
}

// 2: A foo() that accepts all other arguments
template <typename T, typename Enable = enable_if_t<!std::is_base_of_v<Base, T>>>
void foo(T thing) {
    std::cout << "It's not a derived." << std::endl;
}

The compiler complains that foo is multiply defined. The internet tells me that this is because the template arguments aren't relevant when checking function signatures.

Variants I Tried

In my quest to do the most basic metaprogramming possible, I began throwing syntax at the problem. Here's a list of things that I tried for the enable_if statement (inverted statement i.e. !std::is_base_of identical, but omitted for brevity):

Anonymous Type, No typename, Equals 0

https://en.cppreference.com/w/cpp/types/enable_if tells me that what I did above was wrong. But its suggestion (found under the first notes block) is appropriately cryptic, and more importantly, also doesn't compile.

std::enable_if_t<std::is_base_of_v<Base, T>> = 0

Anonymous Type, No typename, Equals void

Thinking that maybe if I'm programming with types, using a type would be a wise choice, I instead tried to default the template to void. No dice.

std::enable_if_t<std::is_base_of_v<Base, T>> = void

Anonymous Type, Yes typename, Equals void

While we're throwing syntax at it, if I'm defaulting this template parameter to a type, shouldn't I use the typename keyword?

typename std::enable_if_t<std::is_base_of_v<Base, T>> = void

What Finally And Oh So Obviously Worked

typename enable_if_t<std::is_base_of_v<Base, T>, T>* = nullptr

I've asked everyone I know why this works yet my other variants don't, and they are equally confused. I'm at a loss. To make matters more confusing, if I name this type (e.g. typename Enable = ...), it fails to compile.

I would be extremely grateful if one who is more familiar with TMP and enable_if would explain to me:

  1. Why does declaring the enable_if as a pointer to a type and defaulting it to nullptr work?
  2. What are the semantic rules for defaulting enable_if?
  3. What are the semantic rules for naming types produced by enable_if?
  4. Is there a reference I can use which clearly explains this and other rules like it in template-land?

Many thanks.

Galactic answered 3/8, 2019 at 15:57 Comment(2)
void is not a valid value for the non-type template parameter to the left of = "if I name this type" - it is not a type, typename enable_if_t<std::is_base_of_v<Base, T>, T>* would be a pointer to T value that requires a valid pointer value as an initializer, not some typeImprobability
The example on cppreference does compile. You changed it when putting it into your code by choosing a type that doesn't have 0 as a valid initializer.Tissue
C
9

The first set of variants you are just setting the value of a template type argument. Two overloads with different values for a template type argument collide, as they are both of kind template<class,class> and have the same function arguments.

The non-type template argument cases, the ones where you use a raw enable if you end up having a template non type argument of type void. That is illegal; the various error messages are the various ways it is illegal.

When you add a star, when the enable if clause passes it is a template non type argument of type void pointer.

When it fails, it isn't an argument at all.

An equivalent to the nullptr case is:

std::enable_if_t<std::is_base_of_v<Base, T>, bool> = true

when the clause is true, the enable if evaluates to bool, and we get:

bool = true

a template non-type argument of type bool that defaults to true. When the clause (the base of clause) is false, we get a SFINAE failure; there is no template type or non-type argument there.


With the class Whatever = enable_if cases we are trying SFINAE based on default value of template arguments. This leads to signature collision, because signatures have to be unique if they are found during overload resolution (in the same phase).

With the enable = value cases, we are trying SFINAE based on if there is a template non-type argument there. On failure, there is no signature to compare, so it cannot collide.

What remains is to make the syntax simple and pretty.

Now, this is all obsolete with Concepts, so don't fall in love with the syntax.

Chimere answered 3/8, 2019 at 17:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.