std::enable_if<>

Why we need std::enable_if<>

To lead to std::enable_ifstd::enable\_if, let's see an example first. Suppose we have a class Person.

#ifndef Person_hpp
#define Person_hpp

#include <string>
#include <type_traits>
#include <iostream>

class Person{
public:
    Person(const std::string& s):name(s){
        std::cout<<"Constructor for const std::string& "<<std::endl;
    }
    Person(std::string&& s):name(std::move(s)){
        std::cout<<"Constructor for std::string&& "<<std::endl;
    }
    
    Person(const Person& other):name(other.name){
        std::cout<<"Constructor for const Person& "<<std::endl;
    }
    
    Person(Person&& other):name(std::move(other.name)){
        std::cout<<"Constructor for Person&& "<<std::endl;
    }
private:
    std::string name;
};

#endif /* Person_hpp */

That's pretty strarght forward. Just some constructors.

#include <iostream>
#include "Person.hpp"
using namespace std;

int main(int argc, const char* argv[]){
    string s = "tom";
    Person p1{s};
    Person p2{"tom"};
    Person p3{p1};
    Person p4{std::move(p1)};
}

And the output is

Constructor for const std::string& 
Constructor for std::string&& 
Constructor for const Person& 
Constructor for Person&& 
Program ended with exit code: 0

Fair enough.

And now, let's try to replace two stringstring constructor with one constructor perfect forwarding the passed argument.

(If you're not familiar with std::forward(), you may want to check this first.)

We change our Person class to this

#ifndef Person_hpp
#define Person_hpp

#include <string>
#include <type_traits>
#include <iostream>

class Person{
public:
    template <typename STR>
    Person(STR&& str):name(std::forward<STR>(str)){
        std::cout<<"Constructor for template STR&&"<<std::endl;
    }
    
    Person(const Person& other):name(other.name){
        std::cout<<"Constructor for const Person& "<<std::endl;
    }
    
    Person(Person&& other):name(std::move(other.name)){
        std::cout<<"Constructor for Person&& "<<std::endl;
    }
private:
    std::string name;
};

#endif /* Person_hpp */

and main()

#include <iostream>
#include "Person.hpp"
using namespace std;

int main(int argc, const char * argv[]){
    string s = "tom";
    Person p1{s};
    Person p2{"tom"};
   //Person p3{p1};
    Person p4{std::move(p1)};
    const Person p5("tom");
    Person p6{p5};
}

The output is

Constructor for template STR&&
Constructor for template STR&&
Constructor for Person&& 
Constructor for template STR&&
Constructor for const Person& 
Program ended with exit code: 0

p2{"tom"}p2\{"tom"\} is deduced to of type const  char[4]const~~ char[4], appling std::forward()std::forward() has not much of an effect, the member namename is constructed from a null-terminated char array.

And in constructor for p6p6, p5p5 is deduced to be of type const  Person&const~~ Person\&.

You may notice that p3p3 is commented, because it won't even compile. Because of we overloaded the constructors, and Person(STR&&)Person(STR\&\&) is a better match than Person(const Person&)Person(const~Person\&) since the reference toPersonPerson require a 'const' restriction, and the type STRSTR is just substituted with Person&Person\&. And we call

template <typename STR>
Person(STR&& str):name(std::forward<STR>(str)){
    
}

with a Person&Person\&, we try to constructor a std::stringstd::string with a PersonPerson, sure there's no matching constructor.

You may think of adding a nonconst copy constructor like

Person(Person& other);

It's indeed a solution, but it's only a partial solution. Because for a derived class, the derived class will inherit the member template, and if the derived class has it's own copy constructor, the copy constructor of derived will never be called since the member template is always a better match.

What you really want is to disable the member function if unless the parameter can be converted to std::stringstd::string, and here come std::enable_ifstd::enable\_if.

std::enable_if<>

Let's first see an example

#include <iostream>
#include <type_traits>
using namespace std;

template <typename T>
typename std::enable_if<(sizeof(T)>2),void>::type foo(){
    std::cout<<"enabled"<<std::endl;
}

int main(int argc, const char * argv[]){
    
     
}

The function foo()foo() is enabled only if we instantiate it with a type with length larger than 2. In main()main(), if we write

foo<char>();

It won't compile. But if we write

foo<int>();

it compiles and the output is

enabled
Program ended with exit code: 0

To understand how to use something, the best way is reading the source code. The STL implement it as following

template <bool, class _Tp = void> struct _LIBCPP_TEMPLATE_VIS enable_if {};
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS enable_if<true, _Tp> {typedef _Tp type;};

#if _LIBCPP_STD_VER > 11
template <bool _Bp, class _Tp = void> using enable_if_t = typename enable_if<_Bp, _Tp>::type;
#endif

Let's only look at the juicy part.

template <bool, class _Tp = void> struct  enable_if {};
template <class _Tp> struct enable_if<true, _Tp> {typedef _Tp type;};


template <bool _Bp, class _Tp = void> using enable_if_t = typename enable_if<_Bp, _Tp>::type;

It's again the trick of template partial instantiation.

STL declares a struct enable_ifenable\_if with a non-type template parameter of type bool, and a type parameter _Tp\_Tp.

If the first parameter is true, STL provides a instantiation and inne type typetype same as the second type parameter, it can be the return type of our function, we use it with

typename std::enable_if<true,someType>::type

And if the parameter is false, there's no instantiation, and when we try to access enable_if<bool,someType>::typeenable\_if<bool,someType>::type, it sure won't compile.

And we can see STL also provided a way to access the inner type with less code. The type alias. We can write

template <typename T>
std::enable_if_t<true,someType> foo(){
    
}

Now, we can fix the problem in class PersonPerson.

class Person{
public:
    
    
    template <typename STR,
        typename = std::enable_if_t<
            std::is_convertible_v<STR, std::string>
        >
    >
    Person(STR&& str):name(std::move(str)){
        
    }
    

    Person(const Person& other):name(other.name){
        std::cout<<"Constructor for const Person& "<<std::endl;
    }
    
    Person(Person&& other):name(std::move(other.name)){
        std::cout<<"Constructor for Person&& "<<std::endl;
    }
private:
    std::string name;
};

It seems to be messy, we can write like

class Person{
public:
    
    template <typename STR>
    static const bool ConvertibleToString = std::is_convertible_v<STR, std::string>;
    
    template <typename STR>
    using EnableIfConvertibleToString = std::enable_if_t<ConvertibleToString<STR>,void>;
    
    template <typename STR,typename = EnableIfConvertibleToString<STR>>
    Person(STR&& str):name(std::move(str)){
        
    }
    
    Person(const Person& other):name(other.name){
        std::cout<<"Constructor for const Person& "<<std::endl;
    }
    
    Person(Person&& other):name(std::move(other.name)){
        std::cout<<"Constructor for Person&& "<<std::endl;
    }
private:
    std::string name;
};

And now in main()main()

int main(int argc, const char * argv[]){
    
    Person p1{"hi"};
    Person p2{p1};
}

The output is

Constructor for STR&&
Constructor for const Person& 
Program ended with exit code: 0

When we try to call copy constructor with a type which is converible to std::stringstd::string,since the member template, we call Person<STR,void>()Person<STR, void>(); and if we call copy constructor with type PersonPerson, the compiler find the match of Person(const Person&)Person(const~Person\&).

Hah, template , what a magic thing !!!