From the many changes of C++20, let's focus today on explicit(bool)
. This feature is described in P0892R2. The explicit
keyword avoids implicit conversions by unexpectedly call a constructor. With C++20, it is now possible to have boolean condition as a "parameter" to the keyword.
How it works
This simple code makes it easy to understand how explicit(bool)
works:
constexpr bool ENABLE_EXPLICIT = false;
struct Foo {
explicit(ENABLE_EXPLICIT)
Foo(int) {}
};
Foo a = 1;
This code compiles fine, but change ENABLE_EXPLICIT
to true
and you will get an error with clang-10.0.0:
error: no viable conversion from 'int' to 'Foo'
note: explicit constructor is not a candidate (explicit specifier evaluates to true)
explicit(bool)
is that simple.
Purpose
OK but the code above is quite non-sense. Why would one need explicit(bool)
?
In template types, it is often desirable to make constructors explicit or implicit based on certain properties of the template parameters. The proposal P0892R2 shows an example with std::pair
:
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&& );
};
Depending on what are T1, T2, U1 and U2:
- It may or may not be possible to create an instance of
std::pair<T1, T2>
from two objects of typesU1
andU2
- The constructor may be explicit or implicit.
Thanks to the new explicit(bool)
feature, the code above can be rewritten as:
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&& );
};
The code is clearly improved. First, there is only one constructor so it avoids code duplication. Secondly, the condition that makes the constructor explicit is separated from the condition that makes the constructor viable. See "PS" at the end of this article for more explanations about these conditions.
Other examples
This answer by Jarod42 on stackoverflow shows that it is possible to make variadic constructors explicit only if there is one parameter in the pack:
struct Foo {
template <typename ... Ts>
explicit(sizeof...(Ts) == 1) Foo(Ts&&...) {}
};
Foo good = {1,2}; // OK
Foo bad = {1}; // error: chosen constructor is explicit in copy-initialization
In their article "C++20โs Conditionally Explicit Constructors", Sy Brand from Microsoft uses explicit(bool)
to write wrappers for strings and takes advanges of explicit(bool)
to simplify their code, in a very similar manner as in the example of the proposal. They invoke the principle of least astonishment as a motivation for wrappers to behave in a similar way as the wrapped types.
Conclusion
explicit(bool)
is clearly a feature for people who write template types. It is likely that you won't use it in you everyday life but it may help you understand code written by others.
PS
I think it is interesting to understand why the constructor of pair
is declared as:
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&& );
};
There are 2 things to understand: the usage of std::enabled_if_t
andย the condition in explicit
. Buckle up!
1) Why is std::enabled_if_t used here?
Let's consider a simple code like this:
template <typename T>
struct Foo {
template<typename U>
Foo(U u) : t(u) {}
T t;
};
Foo<int> good = 1;
Foo<int> bad = "1";
This code generates of an error for bad
:
error: cannot initialize a member subobject of type 'int' with an lvalue of type 'const char *'
note: in instantiation of function template specialization 'Foo<int>::Foo<const char *>'
The error occurs in the "body" of the constructor. It would be nice to catch the error earlier by rejecting the call to the constructor. This can be done thanks to std::enable_if
:
#include <type_traits>
template <typename T>
struct Foo {
template<typename U,
std::enable_if_t<std::is_constructible_v<T, U>, int> = 0>
Foo(U u) : t(u) {}
T t;
};
The error becomes:
error: no viable conversion from 'const char [2]' to 'Foo<int>'
note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'const char [2]' to 'const Foo<int> &'
note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'const char [2]' to 'Foo<int> &&'
note: candidate template ignored: requirement 'std::is_constructible_v<int, const char *>' was not satisfied [with U = const char *]
Noice! ๐
Let's understand how this works.
template<typename U, std::enable_if_t<std::is_constructible_v<T, U>, int> = 0>
is equivalent to template<typename U, SOMETHING = 0>
where SOMETHING
is std::enable_if_t<std::is_constructible_v<T, U>, int>
.
cppreference explains how std::enable_if
works:
Defined in header
<type_traits>
template< bool `B`, class T = void > struct enable_if;
If B is true,
std::enable_if
has a public member typedef type, equal toT
; otherwise, there is no member typedef.
In our case, the boolean condition B
is std::is_constructible_v<T, U>
and the typedef type is int
.
Hence:
- If
T
can be constructed fromU
, thenSOMETHING
isint
. The constructor has a second template parameter of typeint
, which is defaulted to 0, and the constructor can be called. - Otherwise, the template definition is ill-formed, and thanks to SFINAE, this constructor is removed from the overloads. The other overloads are the compiler-defined move and copy constructors but there are not acceptable neither.
At the end of the day, there is a viable conversion from 1
to Foo
but there isn't from "1"
to Foo
and we get a clear error.
We can now get back to std::pair
's constructor and easily understand it: it's the same as Foo
but with an additional template type.
2) How is the constructor made explicit?
std::pair
's constructor is explicit if the following condition is true: !std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>
. It means that if U1
and/or U2
cannot be implicitly converted to T1
and T2
respectively, then the constructor is explicit.
This behavior follows the principle of least astonishment: it would be astonishing if a pair of {U1, U2}
could be implicitly converted to pair of {T1, T2}
if U1
and/or U2
cannot be converted to T1
and T2
implicitly.
Example with a custom Pair
type:
template <typename T>
struct Foo {
explicit Foo(T) {}
};
template <typename T>
struct Pair {
template <typename U>
Pair(U, U) {}
};
Pair<Foo<int>> pair = {1, 2}; // OK
Foo<int> foo1 = 1; // error: no viable conversion from 'int' to 'Foo<int>'
Foo<int> foo2 = 2; // error: no viable conversion from 'int' to 'Foo<int>'
So... We can create a pair of Foo<int>
from two int
s but we can't create two Foo
s from two int
s? It's quite weird and we can use explicit(bool)
(as in std::pair
) to avoid that:
template <typename T>
struct Pair {
template <typename U>
explicit(not std::is_convertible_v<U, T>)
Pair(U, U) {}
};
Pair<Foo<int>> pair = {1, 2}; // error: chosen constructor is explicit in copy-initialization
If we remove the explicit
keyword from the definition of Foo::Foo(T)
, the error disappears and we can create pair
from {1, 2}
.
That's it!
I hope you enjoyed this trip into the details of the declaration of std::pair
's constructor as much as I have! ๐
Top comments (0)