what is the usecase for explicit (bool)
Asked Answered
M

3

24

C++20 introduced explicit (bool) which conditionally selects at compile-time whether a constructor is made explicit or not.

Below is an example which I found here.

struct foo {

  // Specify non-integral types (strings, floats, etc.) require explicit construction.

  template <typename T>

  explicit(!std::is_integral_v<T>) foo(T) {}

};

foo a = 123; // OK

foo b = "123"; // ERROR: explicit constructor is not a candidate (explicit specifier evaluates to true)

foo c {"123"}; // OK

Can anyone tell me any other usecase for explicit (bool) other than using std::is_integral?

Metrology answered 12/3, 2020 at 1:15 Comment(3)
One example is it becomes a lot easier to implement conditionally explicit constructors like those for tuple with this feature.Hick
Not a proper answer, but you could also look at the motivation in the paper that introduced it: wg21.link/p0892Byelaw
Example: it (along with concepts) cuts down the required number of base classes to implement a conditionally provided conditionally explicit copy constructor from 3 to 0.Xenophobe
H
23

The motivation itself can be seen in the paper.

There is a need to make constructors conditionally explicit. That is, you want:

pair<string, string> safe() {
    return {"meow", "purr"}; // ok
}

pair<vector<int>, vector<int>> unsafe() {
    return {11, 22}; // error
}

The former is fine, those constructors are implicit. But the latter would be bad, those constructors are explicit. With C++17 (or C++20 with concepts), the only way to make this work is to write two constructors - one explicit and one not:

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&& );    
};  

These are almost entirely duplicated - and the definitions of these constructors would be identical.

With explicit(bool), you can just write a single constructor - with the conditionally explicit part of the construction localized to just the explicit-specifier:

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&& );   
};

This matches intent better, is much less code to write, and is less work for the compiler to do during overload resolution (since there are fewer constructors to have to pick between).

Hokusai answered 12/3, 2020 at 1:25 Comment(1)
C++20 also provides the ability to change the enable_if_t part to a prettier and simpler constraint, possibly using concepts. But that's beside the point of this question.Trichomoniasis
W
7

Another possible usage I see is with variadic template:

It is generally good, by default, to have explicit for constructor with only one argument (unless the conversion is desired).

so

struct Foo
{
    template <typename ... Ts>
    explicit(sizeof...(Ts) == 1) Foo(Ts&&...);

    // ...
};
Warlock answered 12/3, 2020 at 8:46 Comment(4)
This implies that there is a downside to declaring constructor Foo explicit when sizeof...(Ts) > 1, could you elaborate on this?Egest
@303: Foo make_foo() { return {1, 2}; } would not be possible...Warlock
Alright, but that can simply be rewritten as auto make_foo() { return Foo{1, 2}; } due to the copy-elision rules, no? I understand it's about enabling copy-initialization when sizeof...(Ts) > 1, but I could see why this behavior might rather come as a surprise. Is there a scenario where this usage becomes really essential?Egest
@303: Yes, you can always force to be explicit (notice that above also include default constructor). but it would be less convenient for some kind of initialization of class (as container of " explicit pair": std::map<T1, T2> m = {std::pair<T1, T2>{..}, std::pair<T1, T2>{..}, ..}).Warlock
S
0

I could see a use case for requiring explicit conditionally when the input might be a view-like type (raw pointer, std::string_view) that the new object will hold on to after the call (only copying the view, not what it refers to, remaining dependent on the lifetime of the viewed object), or it might be a value-like type (takes ownership of a copy, with no external lifetime dependencies).

In a situation like that, the caller is responsible for keeping the viewed object alive (the callee owns a view, not the original object), and the conversion should not be done implicitly, because it makes it too easy for the implicitly created object to outlive the object it views. By contrast, for value types, the new object will receive its own copy, so while the copy might be costly, it won't make the code wrong if an implicit conversion occurs.

Selfhood answered 12/3, 2020 at 1:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.