Constructor Conditionally Marked Explicit

Constructor conditionally marked explicit

The proposal that added that N4387: Improving pair and tuple, revision 3 has an example of how it works:

Consider the following class template A that is intended to be used
as a wrapper for some other type T:

#include <type_traits>
#include <utility>

template<class T>
struct A {
template<class U,
typename std::enable_if<
std::is_constructible<T, U>::value &&
std::is_convertible<U, T>::value
, bool>::type = false
>
A(U&& u) : t(std::forward<U>(u)) {}

template<class U,
typename std::enable_if<
std::is_constructible<T, U>::value &&
!std::is_convertible<U, T>::value
, bool>::type = false
>
explicit A(U&& u) : t(std::forward<U>(u)) {}

T t;
};

The shown constructors both use perfect forwarding and they have
essentially the same signatures except for one being explicit, the
other one not. Furthermore, they are mutually exclusively constrained.
In other words: This combination behaves for any destination type T
and any argument type U like a single constructor that is either
explicit or non-explicit (or no constructor at all).

As Praetorian points out this is exactly how libstdc++ implements it.

If we modify the OPs example accordingly, it also works:

struct S {
template <typename T,
typename std::enable_if< std::is_integral<T>::value, bool>::type = false>
S(T) {}

template <typename T,
typename std::enable_if<!std::is_integral<T>::value, bool>::type = false>
explicit S(T) {}
};

How can I apply make a default constructor conditionally explicit?


  1. What is a possible implementation?

Have you tried with

template <class T>
class C {
public: // VVVVVVVVVVVVVV .................................V U here, not T
template <typename U = T, std::enable_if_t<std::is_same_v<U, int>, int> = 0>
C() {}

template <typename U = T, std::enable_if_t<!std::is_same_v<U, int>, int> = 0>
explicit C() {}
};

?

  1. Why does this not compile?

The problem is that SFINAE, over class methods, works with the template parameters of the methods itself.

That is in the original working code:

  template <typename T,
typename std::enable_if<std::is_integral<T>::value, bool>::type = false >
S(T) {}

where T is a template parameter specific of the constructor (deduced from the single argument).

On the contrary, in your failing code,

template <std::enable_if_t<std::is_same_v<T, int>, int> = 0>
C() {}

the constructors are evaluating the template parameter of the class (T), not of the methods.

With the trick typename U = T, you transform T, the template parameter of the class, in U, a template parameter of the methods (constructors in your case, but works also with other methods) so std::enable_if_t, with a test depending from U, is able to enable/disable the constructors.

When you create a new class based on QObject, why does the wizard mark your constructor explicit?

It is used for single parameter constructors as that is when it makes sense.

This way, you disable implicit conversion from a type to a different so that the compiler will generate an error for you.

This makes sense in certain cases when you want to make your API user think and make the conversion explicitly.

This can catch accidental errors in the code when the user is not actually meant to get an implicit conversion.

QSlider *s = new QSlider();
Sneed *s1 = new Sneed();
Sneed *s2 = s; // Typo: wanted to copy s1. Compiler catches.

Here is a more detailed explanation for the whole topic by KDAB: https://www.youtube.com/watch?v=GUdQ9u34HQI

Here is a little more complete example:

class String
{
public:
String(const char *); // Should be implicit
String(int size); // Should be explicit
};
void print(const String &s);

int main()
{
String s = "hello";
print(s); // All good.
print("hello"); // All good.
String preallocated(123);
print(preallocated); // All good.
print(123); // Not printing 123 as intuitively expected.
}

QObject example:

class MyWidget : public QWidget
{
public:
MyWidget(QObject *parent = nullptr)
: QWidget(parent)
{}

QString getData() const { return "some data"; }
};

MyWidget *createWidget()
{
return new MyWidget;
}

void testWidget1()
{
// Accidentally created on the stack. How can it compile?
// Implicit conversion, parented rather than taking the pointer.
MyWidget w = createWidget();
}

This can become even worse when mixing unrelated QObject (QWidget in this case) subclasses.

void testWidget2()
{
QPushButton *w1 = new QPushButton;
// Unrelated push button, parented, not meant it.
MyWidget *w2 = w1;
}

Even more challenging to spot the mistake in the following example that a temporary is created.

MyWidget testWidget3()
{
QPushButton *w1 = new QPushButton;
// If the function is long, you will not even see the
// return type here, just assume it is MyWidget*.
// How come I do not see the widget despite showVisible(), etc?
return w1;
}

Yet another example when a function or method parameter would accidentally use const reference when it should use the idiomatic Qt way, which is a pointer in this case.

void debug(const MyWidget &w) { qDebug() << w.getData(); }

void testWidget4() {
// temporary again, no pointer.
debug(new QPushButton);
}

Should such constructors be always explicit? No. Sometimes, it makes sense to have them implicit. For example, it would be inconvenient having to convert a const char * explicitly to a String as in the above example. The decision should go on a case-by-case basis for constructors.

Explicit can be a good default though if you are unsure as you can always open up later also to implicit conversion.

Some consider that the default C++ behaviour ought to be explicit rather than implicit conversion.

Either way, one would still need to understand the differences and consider the decision on a case-by-case basis.

Also, even if you want to opt out, you should consider using Q_IMPLICIT (macro expanding to nothing) in Qt 6 to mark your intention so that when anyone reviews your code in the future, they can notice that it is deliberate rather than an accidental omission of explicit.

In C++20, you can even use the conditional explicit keyword, as explicit(false) instead of Q_IMPLICIT.

clang-tidy can also check it for you whether it is clearly defined rather than left uncertain.

I would also like to mention some cases where an explicit conversion required instead of accepting implicit conversion could become inconvenient.

void drawLine(QPoint a, QPoint b);
drawLine({0, 0}, {5, 5});

If you needed to draw thousands of lines, the explicit conversion would become really cumbersome.

So, the trade-off is always convenience vs. catching accidental mistakes. In the above QPoint example, this is likely not very error-prone to accidental mistakes.

The constructor should be implicit if the source data type is a faithful representation of the target type. const char * and String pretty much represent the same.

But how an integer would map to a String can be more ambiguous. The entities represent different things. So, the constructor ought to be explicit.

Are std::tuple and std::tuple<std::tuple> considered same type by std::vector?

Tuples and vectors are mostly red herrings here. The way that works is simply that push_back, like any function, can perform implicit conversions on its argument, as demonstrated by the following working snippet:

#include <vector>

struct A { };

struct B {
B(A const &) { }
};

int main() {
std::vector<B> v;
v.push_back(A{});
}

Going back to tuple, we can see that it has (among others) a conditionally-explicit constructor (#2 here) which takes references to the tuple's members-to-be:

tuple( const Types&... args );

This constructor is implicit if and only if all of the members have implicit copy constructors, which is the case here (as synthesized constructors are implicit indeed). This means that std::tuple<...> is implicitly convertible to std::tuple<std::tuple<...>>, which is what you're observing.

Is it possible to deprecate implicit conversion while allowing explicit conversion?

You can turn Duration(int t_seconds) into a template function that can accept an int and set it to deprecated.

#include<concepts>

class Duration {
int seconds;
public:
template<std::same_as<int> T>
[[deprecated("uses implicit conversion")]]
Duration(T t_seconds) : Duration(t_seconds) { }

explicit Duration(int t_seconds) : seconds(t_seconds) { }
};

If you allow t = 0.6, just change the same_as to convertible_to.

Demo.

Why C++ implicit conversion works, but explicit one does not?

Here's a simplified code that reproduces the same issue:

struct S
{
template <typename T>
operator T() // non-explicit operator
{ return T{}; }
};

struct R
{
R() = default;
R(const R&) = default;
R(R&&) = default;
R(int) {} // problematic!
};

int main()
{
S s{};
R r = static_cast<R>(s); // error
}

We can see the compile error is similar:

error: call of overloaded 'R(S&)' is ambiguous
R r = static_cast<R>(s);
^
note: candidates...
R(int) {}
R(R&&) = default;
R(const R&) = default;

The problem relies on the generic S::operator T(), which will happily return a value to whatever type you want. For example, assigning s to any type will work:

int i = s; // S::operator T() returns int{};
std::string str = s; // S::operator T() returns std::string{};

T is deduced to the conversion type. In the case of std::string, it has a lot of constructors, but if you do a copy-initialization(1) of the form object = other, T is deduced to the left-hand object's type (which is std::string).

Casting is another matter. See, it's the same problem if you try to copy-initialize using the third form (which in this case is a direct initialization):

R r(s); // same ambiguity error

Okay, what are the constructor overloads for R again?

R() = default;
R(const R&) = default;
R(R&&) = default;
R(int) {}

Given that R's constructors can take either another R, or int, the problem becomes apparent, as the template type deduction system doesn't know which one of these is the correct answer due to the context in which the operator is called from. Here, direct initialization has to consider all the possible overloads. Here's the basic rule:

A is the type that is required as the result of the conversion. P is the return type of the conversion function template

In this case:

R r = s;

R is the type that is required as the result of the conversion (A). However, can you tell which type A will represent in the following code?

R r(s);

Now the context has R and int as options, because there is a constructor in R that takes integers. But the conversion type needs to be deduced to only one of them. R is a valid candidate, as there is at least one constructor which takes an R. int is a valid candidate as well, as there is a constructor taking an integer too. There is no winner candidate, as both of them are equally valid, hence the ambiguity.

When you cast your json object to an std::string, the situation is exact the same. There is a constructor that takes an string, and there is another one that takes an allocator. Both overloads are valid, so the compiler can't select one.

The problem would go away if the conversion operator were marked as explicit. It means that you'd be able to do std::string str = static_cast<std::string>(json), however you lose the ability to implicitly convert it like std::string str = json.

Why is this combination of move and value constructor ambigous for MSVC but not for Clang and GCC in C++17 and above

This is CWG 2327, which doesn't currently have a resolution but gcc and clang seem to do "the right thing." The issue reads:

Consider an example like:

 struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d);

This goes to 9.4 [dcl.init] bullet 17.6.2:

Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (12.2.2.4 [over.match.ctor]), and the best one is chosen through overload resolution (12.2 [over.match]). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

Overload resolution selects the move constructor of Cat. Initializing the Cat&& parameter of the constructor results in a temporary, per 9.4.4 [dcl.init.ref] bullet 5.2.1.2. This precludes the possitiblity of copy elision for this case.

This seems to be an oversight in the wording change for guaranteed copy elision. We should presumably be simultaneously considering both constructors and conversion functions in this case, as we would for copy-initialization, but we'll need to make sure that doesn't introduce any novel problems or ambiguities.

The case here is similar, just more complicated. With either:

R r0(s);
R r = static_cast<R>(s);

We have a conversion function from s that gives a prvalue R, which is a better path to take than any of the other paths at our disposal -- whether doing through R's move constructor or R(int). But we just don't have a rule that says that that's what we're supposed to do.

gcc and clang seem to implement the desired behavior on C++17 or later (where we have guaranteed copy elision) despite not having any wording for what the desired behavior is. msvc on the other hand seems to follow the rules as specified (which I think do specify this case as being ambiguous).

Does Visual Studio 2017 need an explicit move constructor declaration?

Let's look at the std::vector source code (I replaced pointer and _Ty with actual types):

void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, true_type)
{ // move [First, Last) to raw Dest, using allocator
_Uninitialized_move(First, Last, Dest, this->_Getal());
}

void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, false_type)
{ // copy [First, Last) to raw Dest, using allocator
_Uninitialized_copy(First, Last, Dest, this->_Getal());
}

void _Umove_if_noexcept(Node* First, Node* Last, Node* Dest)
{ // move_if_noexcept [First, Last) to raw Dest, using allocator
_Umove_if_noexcept1(First, Last, Dest,
bool_constant<disjunction_v<is_nothrow_move_constructible<Node>, negation<is_copy_constructible<Node>>>>{});
}

If Node is no-throw move-constructible or is not copy-constructible, _Uninitialized_move is called, otherwise, _Uninitialized_copy is called.

The problem is that the type trait std::is_copy_constructible_v is true for Node if you do not declare a move constructor explicitly. This declaration makes copy-constructor deleted.

libstdc++ implements std::vector in a similar way, but there std::is_nothrow_move_constructible_v<Node> is true in contrast to MSVC, where it is false. So, move semantics is used and the compiler does not try to generate the copy-constructor.

But if we force is_nothrow_move_constructible_v to become false

struct Base {
Base() = default;
Base(const Base&) = default;
Base(Base&&) noexcept(false) { }
};

struct Node : Base {
std::unordered_map<int, std::unique_ptr<int>> map;
};

int main() {
std::vector<Node> vec;
vec.reserve(1);
}

the same error occurs:

/usr/include/c++/7/ext/new_allocator.h:136:4: error: use of deleted function ‘std::pair<_T1, _T2>::pair(const std::pair<_T1, _T2>&) [with _T1 = const int; _T2 = std::unique_ptr<int>]’
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Related Topics



Leave a reply



Submit