Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++20

explicit(To Be || !(To Be)) - Discussion and Usage of explicit(bool) C++20

0.00/5 (No votes)
22 Apr 2023CPOL2 min read 3.4K  
More about explicit(bool) C++20
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:

C++
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:

C++
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:

C++
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 p = "Elizabeth"; // won't compile
    person p1("Elizabeth");
    p1.walk();
    // perform_something("ABC"); // won't compile
    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:

C++
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:

C++
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:

C++
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(3.4); // won't compile
    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:

C++
std::pair<std::string, std::string> safe() {
    return {"meow", "purr"}; // ok
}
std::pair<std::vector<int>, std::vector<int>> unsafe() {
    return {11, 22}; // error
}

In order to do that, the std::pair constructor may look like this:

C++
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:

C++
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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)