std::enable_if<>
Why we need std::enable_if<>
To lead to , 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 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
is deduced to of type
, appling
has not much of an effect, the member
is constructed from a null-terminated char array.
And in constructor for ,
is deduced to be of type
.
You may notice that is commented, because it won't even compile. Because of we overloaded the constructors, and
is a better match than
since the reference to
require a 'const' restriction, and the type
is just substituted with
. And we call
template <typename STR>
Person(STR&& str):name(std::forward<STR>(str)){
}
with a , we try to constructor a
with a
, 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 , and here come
.
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 is enabled only if we instantiate it with a type with length larger than 2.
In
, 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 with a non-type template parameter of type bool, and a type parameter
.
If the first parameter is true, STL provides a instantiation and inne type 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 , 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 .
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
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 ,since the member template, we call
; and if we call copy constructor with type
, the compiler find the match of
.
Hah, template , what a magic thing !!!