In this post, you will see feature discussion, motivation, usage example, proposal and more of explicit(bool) C++20. You will also get a basic explanation about explicit and implicit conversions pros and cons.
If William Shakespeare would have been a C++ developer, this would probably be the title that the explicit
(bool
) proposal would have got. Unfortunately, he did the right decision and decided !(to be) before computers invented. But why is this C++20 feature so important?
explicit Keyword
Sometimes, we want to forbid implicit conversion from one type to a custom structure we created. Implicit conversion might cause invisible bugs or side effects that might be difficult to trace. Here is an example for implicit conversion:
class integer {
public:
Integer(int i) : val(i) {}
void print() { std::cout << val << "\n"; }
private:
int val;
};
{
integer i;
i = 5;
i.print();
}
In this case, the behaviour is clear. There is a direct connection between integer
class and int
, so the implicit conversion makes sense. However, let’s take, for example, the following person
class:
class person {
public:
person(const std::string& name) : name(name) {}
void walk() {}
void write_blog() {}
private:
std::string name;
};
void perform_something(const person& p) {}
{
person p = "Elizabeth";
p.walk();
perform_something("ABC");
}
This time, we wouldn’t want to allow such conversation. It’s very unclear what we actually pass to perform_something
function, especially if this function is declared on another header. Therefore, we got the explicit
keyword:
class person {
public:
explicit person(const std::string& name) : name(name) {}
void walk() {}
void write_blog() {}
private:
std::string name;
};
void perform_something(const person& p) {}
{
person p1("Elizabeth");
p1.walk();
perform_something(person("ABC"));
}
explicit(bool)
This ability is important in cases that we construct our structure using template parameters. In such case, we sometimes might want to enable implicit casts, and sometimes, we don’t. Let’s view a simple example for such case:
class integer {
public:
template<typename T, typename = std::enable_if_v<std::is_arithmetic_v<T>>>
integer(T t) : val(std::round(t)) {}
private:
int val;
};
void func(integer i) {}
{
func(1.6);
func(1.4);
func(5);
}
In this case, in the firat call to func
, we send the number 2, and in the second, we send the number 1. Because the trivial behaviour of casting to int
(which will results in sending 1
in both cases) differs from this behaviour, it might lead to unintended results.
However, we wouldn’t want to force the third case to use explicit constructor as well, because the behaviour there is obvious. In order to do so (without using the C++20 feature explicit
(bool
)), we have to split the function into two separate overloads like this:
class integer {
public:
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
integer(T t) : val(t) {}
template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
explicit integer(T t) : val(std::round(t)) {}
private:
int val;
};
Now we have another issue: It’s extremely annoying to duplicate a constructor just to get the precise explicit usage. And that’s where explicit
(bool
) become effective:
class integer {
public:
template<typename T, typename = std::enable_if_v<std::is_arithmetic_v<T>>>
explicit(std::is_floating_point_v<T>)
integer(T t) : val(std::round(t)) {}
private:
int val;
};
void func(integer i) {}
{
func(integer(3.4));
func(5);
}
This way, the constructor will be explicit only if the given type is a floating point type.
Reallife Example
The explicit(bool) proposal example was std::pair
. There are trivial cases when the implicit conversion is allowed, and there are cases when it’s not. For example:
std::pair<std::string, std::string> safe() {
return {"meow", "purr"}; }
std::pair<std::vector<int>, std::vector<int>> unsafe() {
return {11, 22}; }
In order to do that, the std::pair
constructor may look like this:
template <typename T1, typename T2>
struct pair {
template <typename U1=T1, typename U2=T2,
std::enable_if_t<
std::is_constructible_v<T1, U1> &&
std::is_constructible_v<T2, U2> &&
std::is_convertible_v<U1, T1> &&
std::is_convertible_v<U2, T2>
, int> = 0>
constexpr pair(U1&&, U2&& );
template <typename U1=T1, typename U2=T2,
std::enable_if_t<
std::is_constructible_v<T1, U1> &&
std::is_constructible_v<T2, U2> &&
!(std::is_convertible_v<U1, T1> &&
std::is_convertible_v<U2, T2>)
, int> = 0>
explicit constexpr pair(U1&&, U2&& );
};
As we can see, the only difference between the two constructors are the convertible conditions and the explicit
keyword. Using explicit
(bool
), we can compress it to a single function:
template <typename T1, typename T2>
struct pair {
template <typename U1=T1, typename U2=T2,
std::enable_if_t<
std::is_constructible_v<T1, U1> &&
std::is_constructible_v<T2, U2>
, int> = 0>
explicit(!std::is_convertible_v<U1, T1> ||
!std::is_convertible_v<U2, T2>)
constexpr pair(U1&&, U2&& );
};
explicit(true) Conclusion
Being explicit about our intention is important in perspective of safety and code readability. Sometimes, we might want to use the explicit term of explicit
(true
) or explicit
(false
) in order to clarify our intentions when creating a new structure type. Now, all you have to do, is to go and spread the word about it!