Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 479
tennessee_jed Avatar asked Aug 03 '19 15:08

tennessee_jed


People also ask

What is the purpose of Enable_if?

enable_if is an extremely useful tool. There are hundreds of references to it in the C++11 standard template library. It's so useful because it's a key part in using type traits, a way to restrict templates to types that have certain properties. Without enable_if, templates are a rather blunt "catch-all" tool.

What is enable if?

Introduction. The enable_if family of templates is a set of tools to allow a function template or a class template specialization to include or exclude itself from a set of matching functions or specializations based on properties of its template arguments.


1 Answers

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.

like image 118
Yakk - Adam Nevraumont Avatar answered Sep 18 '22 02:09

Yakk - Adam Nevraumont