Today I want to share with you something that really surprised me. Currently, Tomasz Kamiński and Ville Voutilainen are working on fixing a certain issue with std::optional ’s converting constructors (which deserves a separate post). At some point, in the solution, they perform the following type-trait test:

is_constructible<T, U>::value || is_convertible<U, T>::value

If the varying order of T and U upsets you, rest assured that this is correct. This is how these traits are defined: is_constructible takes the created type first, whereas is_convertible takes the converted-to type second. But what really struck me here is the apparent redundancy. When type U is convertible to T does it not imply that T is constructible from U ? Or in other words, if the following copy-initialization works:

T v = u;

The following direct-initialization:

T v (u);

should also work? Well, this is C++. It turns out that such expectation is not necessarily correct.

Most of developers probably never has to deal with such nuances, but when you design a generic library you want to consider every tiny corner case. We have one here. First, we need to grasp the difference between these two kinds of initialization:

T v1 = u; // copy-initialization T v2 (u); // direct-initialization

A common-sense answer would be “the latter is more powerful because it additionally considers explicit constructors and conversion operators.” True, it considers more constructors, but to ‘consider’ an additional constructor does not necessarily mean ‘successfully use it construction’. Consider the following type definition of a class that tries to represent rational numbers:

class Q { int _num, _den; explicit Q(double r, Rounding round = Rounding::to_nearest); // more ... public: Q(int num, int den = 1) : _num(num), _den(den) {} // more ... };

It is generally a good practice to make every (non-copy, non-move) constructor explicit, unless you really want the constructor to participate in conversions; and this is what we do here. Integer number 2 is a valid unambiguous and intuitive representation of a rational number; and we want it converted to Q where needed. We also have another special and potentially dangerous constructor that we use internally, and definitely do not want to expose it in the interface, and definitely do not want it to participate in conversions.

The result, however, is quite surprising:

Q q1 = 1.0; // compiles Q q2 (1.0); // compiler-error

Of course, you might question that anyone would store an arbitrary double as a rational number, but the following example looks more plausible:

double get_number(); Q q1 = std::floor(get_number()); Q q2 (std::floor(get_number()));

Here, std::floor is making sure that a double is storing an integral number.

Now, to explain what happens here. When we use copy-initialization, the first constructor is not visible because it is declared as explicit , so there is only one constructor to choose from, and it is suitable after the implicit conversion from double to int has been applied.

In direct initialization we need to consider both constructors. And the first one is a better match because it requires no additional conversion from double to int . The fact that it is declared private does not affect how the overload resolution works. Only later, after we have decided which overload will be used, is the access checking performed: and at that point it causes a compiler error.

This shows one important thing: declaring something private doesn’t mean it is an implementation detail; it may affect the type’s interface. Accessing a private function is not the only situation that causes a type to be convertible from but not constructible from another type. Another such situation is when we declare an explicit constructor as deleted.

struct R { explicit R(double, double = 1.0) = delete; R(int num, int den = 1); };

How much contrived such classes are? You probably don’t see them very often. All those examples have the same issue: they declare competing constructors. The second example with class R tries to implement the advice described here. But apparently that advice should be augmented with the following precaution: when you declare a constructor deleted in order to make another constructor less ‘greedy’, the former has to be as explicit as the latter.

There are other implications of the set of constructors as in our class Q . All the ‘factory’ functions will not be able to work with it:

auto p = std::make_shared<Q>(1.0); // doesn't work std::shared_ptr<Q> q {new Q(1.0)}; // doesn't work boost::optional<Q> o; o.emplace(1.0); // doesn't work

So, I would say it is not a good idea to have a type T convertible form some type U but not constructible from U . Good or bad, such types are allowed to exist, and generic libraries like the future std::optional need to handle them, so we might expect a new trait saying “anyhow create-able from U ”; especially that it is possible to construct a T form a U even when both is_constructible<T, U>::value and is_convertible<U, T>::value return false :

struct X { int i; }; int main() { static_assert(!std::is_constructible<X, int>::value && !std::is_convertible<int, X>::value, ""); X x {1}; }