C++20’s Conditionally Explicit Constructors

Sy Brand

Sy

explicit(bool) is a C++20 feature for simplifying the implementation of generic types and improving compile-time performance.

In C++ it is common to write and use types which wrap objects of other types. std::pair and std::optional are two examples, but there are plenty of others in the standard library, Boost, and likely your own codebases. Following the principle of least astonishment, it pays to ensure that these wrappers preserve the behavior of their stored types as much as is reasonable.

Take std::string as an example. It allows implicit conversion from a string literal, but not from a std::string_view:

void f(std::string);

f(“hello”);   //compiles
f(“hello”sv); //compiler error

This is achieved in std::string by marking the constructor which takes a std::string_view as explicit.

If we are writing a wrapper type, then in many cases we would want to expose the same behaviour, i.e. if the stored type allows implicit conversions, then so does our wrapper; if the stored type does not, then our wrapper follows[1]. More concretely:

void g(wrapper<std::string>);

g("hello");   //this should compile
g("hello"sv); //this should not

The common way to implement this is using SFINAE. If we have a wrapper which looks like this[2]:

template<class T>
struct wrapper {
  template <class U>
  wrapper(U const& u) : t_(u) {}

  T t_;
};

Then we replace the single constructor with two overloads: one implicit constructor for when U is convertible to T and one explicit overload for when it is not:

template<class T>
struct wrapper {
  template<class U, std::enable_if_t<std::is_convertible_v<U, T>>* = nullptr>
  wrapper(U const& u) : t_(u) {}
  
  template<class U, std::enable_if_t<!std::is_convertible_v<U, T>>* = nullptr>
  explicit wrapper(U const& u) : t_(u) {}

  T t_;
};

This gives our type the desired behavior. However, it’s not very satisfactory: we now need two overloads for what should really be one and we’re using SFINAE to choose between them, which means we take hits on compile-time and code clarity.explicit(bool) solves both problems by allowing you to lift the convertibility condition into the explicit specifier:

template<class T> 
struct wrapper { 
  template<class U> 
  explicit(!std::is_convertible_v<U, T>) 
  wrapper(U const& u) : t_(u) {} 

  T t_; 
};

Next time you need to make something conditionally explicit, use explicit(bool) for simpler code, faster compile times[3], and less code repetition.

explicit(bool) will be supported in MSVC v14.24[4] (available in Visual Studio 2019 version 16.4), Clang 9, and GCC 9. We’d love for you to download Visual Studio 2019 and give it a try. As always, we welcome your feedback. We can be reached via the comments below or via email (visualcpp@microsoft.com). If you encounter problems with Visual Studio or MSVC, or have a suggestion for us, please let us know through Help > Send Feedback > Report A Problem / Provide a Suggestion in the product, or via Developer Community. You can also find us on Twitter (@VisualC).

  1. I know, implicit conversions are evil. There are some places where they make a big improvement to ergonomics though and leaving choices to users makes our generic types more widely applicable.
  2. std::forward and such omitted for brevity.
  3. I tested 500 template instantiations with Visual Studio 2019 version 16.2 and using explicit(bool) sped up the frontend by ~15%
  4. The feature is supported in MSVC v14.22 (Visual Studio 2019 version 16.2) for builds with /permissive-, but there are some issues for builds which do not use that flag.

 

4 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    Mike Diack

    I hate to go off topic, but at the post:

    CPPConStuff

    I specifically asked if you could later edit it to add the links about what was spoken about at CPPCon (as the majority of your readers would not have made it to CPP Con). Can you do so please?

    Thanks,

    Mike

  • Avatar
    Gene Bushuyev

    Definitely a welcome change. Now when will we get a conditional const? Why do we still need to write identical const/non-const member functions?
    I want to be able to write: struct A { auto f() const(const(this)) { return something; } };