Why is my "Universal" Constructor called twice?

Pages: 12
Look at this code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
template <typename T>
class MyClass
{
public:
	// Copy Constructors
	MyClass(const MyClass& other)
	{
	    cout << "first copy ctor" << endl;
	}

	template <typename U>
	constexpr MyClass(const MyClass<U>& other) : MyClass(other.get())
	{
	    cout << "copy ctor" << endl;
	}

	// Universal Constructor
	template <typename U>
	constexpr MyClass(U&& u) : ptr_(forward<U>(u))
	{
            // U = int*
            // u = int*&&
	    cout << "universal ctor" << endl;
	}	
	
	constexpr T get() const
	{
            cout << "get()" << endl;
	    return ptr_;
	}
 
        constexpr operator T() const 
        { 
            cout << "operator T()" << endl; 
            return get(); 
        }	

private:
	T ptr_;
};

int main()
{	    
    MyClass<int*> obj = new int{50};
    
    return 0;
}


If you execute this code with -fno-elide-constructors you will notice the following output:

universal ctor
operator T()
get()
universal ctor


Let's analyze the situation:

new int{50} is an rvalue of type int* and when the "Universal" Constructor is called, it prints "universal ctor"

Then, I don't know why, ptr_(forward<U>(u)) calls the operator T()

1) Why is ptr_(forward<U>(u)) executed AFTER the cout of the Universal Constructor? Aren't initialization lists execute before the constructor bodies?

2) Why does ptr_(forward<U>(u)) need to call the conversion operator? The conversion operator is there for converting MyClass to int*...

3) Why is the "Universal" Constructor called AGAIN after the operator T()?

Keep in mind that I disabled optimizations

Last edited on
Line 44

 
MyClass<int*> obj = new int{50};

is the same as

 
MyClass<int*> obj = MyClass<int*>(new int{50});


First a temporary object is created by passing the integer pointer to the universal constructor. This explains the first "universal ctor" in the output.

Next thing that happens is that the temporary object is passed to the universal constructor. The conversion operator is called because the temporary object needs to be converted to a int* when ptr_ is initialized. The body of this constructor call is what prints "universal ctor" a second time.
Last edited on
First of all, thank you very much.

MyClass<int*> obj = new int{50};

is the same as

MyClass<int*> obj = MyClass<int*>(new int{50});


THIS actually tricked me.

The Universal Constructor uses an Universal Reference (U&&) which can basically take everything.

I thought that, in the first call of the Universal Constructor, the pointer returned by new was passed!

Why doesn't the Universal Constructor get called with the temporary pointer becoming this way

 
constexpr MyClass(int*&& u) : ptr_(forward<int*>(u))


Instead, a temporary MyClass object is created before calling the Universal Constructor for the first time, so that it becomes:


 
constexpr MyClass(MyClass<int*>&& u) : ptr_(forward<MyClass<int*>>(u))


I mean... why? By passing the pointer returned by new int{50} we could've been avoided the conversion...


P.S: Why if I enable copy elision, I only get

universal ctor

Which steps are skipped with copy elision in this case?
Last edited on
why? By passing the pointer returned by new int{50} we could've been avoided the conversio

Because you chose the syntax T obj = expr; that specified that behavior (prior to C++17). There are two other syntaxes you could have used: T obj(expr); and T obj{expr};, they behave differently.

P.S: Why if I enable copy elision, I only get

universal ctor

Which steps are skipped with copy elision in this case?

You also get that if you enable C++17 in gcc 7.1 and up, even with -fno-elide-constructors, since the semantics of copy-initialization changed. It just creates the object by calling whatever constructor will take the pointer.
Thank you... one more question...

Since we have a Universal Constructor which takes an Universal Reference as parameter... the other Constructors are never called!

The universal constructor with the Universal Reference always wins over the other overloads!

So the other constructors are useless!

[ i took the code from GSL/not_null here https://github.com/Microsoft/GSL/blob/master/include/gsl/gsl ]
Last edited on
> The universal constructor with the Universal Reference always wins over the other overloads!
> So the other constructors are useless!

No. Normal overload resolution continues to apply.
(Note that for function templates, partial ordering is used to determine the best match.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>

template < typename T > struct A
{
    A() { std::cout << "A default constructor\n" ; }
    A( std::string ) { std::cout << "A::A(std::string)\n" ; }
    A( int ) { std::cout << "A::A(int)\n" ; }

    A( const A<T>& ) { std::cout << "A::copy constructor\n" ; }
    A( A<T>& ) { std::cout << "A::copy (unusual:from modifiable) constructor \n" ; }
    A( A<T>&& ) { std::cout << "A::move constructor\n" ; }
    A( const A<T>&& ) { std::cout << "A::move (unusual: from const) constructor\n" ; }

    template < typename U > A( U&& ) { std::cout << "A::universal constructor\n" ; }
};

int main()
{
    A<int> a1 ; // A default constructor
    A<int> a2( std::string("hello") ) ; // A::A(std::string)
    const A<int> a3( 5 ) ; // A::A(int)
    
    std::cout << '\n' ;
    
    A<int> a5(a3) ; // A::copy constructor
    A<int> a4(a2) ; // A::copy (unusual: from modifiable) constructor
    A<int> a6( std::move(a2) ) ; // A::move constructor
    A<int> a9( std::move(a3) ) ; // A::move (unusual: from const) constructor

    std::cout << '\n' ;

    A<int> a7( 'a' ) ; // A::universal constructor
    A<int> a8( "hello" ) ; // A::universal constructor
}

http://coliru.stacked-crooked.com/a/ec3bf4de81b7f177
Thank you.

But then why in my case (GSL/not_null) the Universal Constructor is always called?

1
2
3
4
5
not_null<Derived*> ptr{ new Derived{} };

not_null<Base*> ptr2 = ptr;

not_null<int*> ptr3 = new int{ 5 };


Output:

universal ctor
universal ctor
universal ctor


Last edited on
See line 85: not_null(const not_null<U>& other) delegates to not_null(U&& u)
If I comment out that delegation

1
2
3
4
constexpr not_null(const not_null<U>& other) // : not_null(other.get())
{
	cout << "Copy" << endl;
}


I still get the Universal Constructor called...

1
2
3
not_null<Derived*> ptr{ new Derived{} };
not_null<Base*> ptr2 = ptr;
not_null<int*> ptr3 = new int{ 5 };


Output:

Universal
Universal
Universal


Except for GCC, which will print

Universal
Universal
Universal
Universal
Universal


(I have disabled fno-elide-constructors on both compilers)
Last edited on
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>

template <class T> class not_null
{
    public:

        template <typename U, typename Dummy = std::enable_if_t<std::is_convertible<U, T>::value>>
        constexpr not_null(U&& u) : ptr_(std::forward<U>(u))
        { std::cout << "universal constructor\n" ; }

        template <typename U, typename Dummy = std::enable_if_t<std::is_convertible<U, T>::value>>
        constexpr not_null(const not_null<U>& other) : ptr_(std::forward<U>(other.get()))
        { std::cout << "type-converting constructor\n" ; }

        not_null(const not_null& other) : ptr_(other.get())
        { std::cout << "copy constructor\n" ; }

        constexpr T get() const { return ptr_ ; }

    private:
        T ptr_;
};

int main()
{
    not_null<int*> pi { new int(5) } ; // universal constructor

    not_null<int*> pi2 { pi } ; // copy constructor

    not_null<int*> pi3 { std::move(pi2) } ; // copy constructor



    struct base { virtual ~base() = default ; };
    struct derived : base {};

    not_null<derived*> pd { new derived } ; // universal constructor

    not_null<derived*> pd2 { pd } ; // copy constructor

    not_null<derived*> pd3 { std::move(pd2) } ; // copy constructor


    not_null<base*> pb { new base } ; // universal constructor

    // convert from derived* to base*
    not_null<base*> pb2 { pd } ; // type-converting constructor
}

http://coliru.stacked-crooked.com/a/36399a9a85456c23
http://rextester.com/UPRHCW67005
I took your initializations (in the main function) and pasted them using GSL's not_null class.

http://coliru.stacked-crooked.com/a/5cbb843b8ccd692f

As you can see, nothing but the Universal Constructor is called.

One may say: it doesn't print "Copy" because there's the delegation.

I disabled the delegation! But still... the Universal Constructor is preferred.



Repeat:
a. What you believe is the copy constructor (lines 29-33) is not the copy constructor
b. Const qualifiers are important in overload resolution; we can't just blithely ignore them
http://coliru.stacked-crooked.com/a/335478095b9d547a
Mhm, thanks for the clarification, got that.

But the whathever-constructor it is on lines 29-33... how can I call it?

It looks like I'm not able to print "Copy" because that whathever-constructor is never selected in overload resolution with the delegation disabled
Last edited on
> how can I call it?

Like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <type_traits>

template <class T>
class not_null
{
public:
	static_assert(std::is_assignable<T&, std::nullptr_t>::value, "T cannot be assigned nullptr.");

	template <typename U, typename Dummy = std::enable_if_t<std::is_convertible<U, T>::value>>
	constexpr not_null(U&& u) : ptr_(std::forward<U>(u)) {}

	template <typename U, typename Dummy = std::enable_if_t<std::is_convertible<U, T>::value>>
	constexpr not_null(const not_null<U>& ) 
	{ std::cout << "************************ (1)\n"; }

	template <typename U, typename Dummy = std::enable_if_t<std::is_convertible<U, T>::value>>
	constexpr not_null(  not_null<U>& )
	{ std::cout << "************************ (2)\n"; }

	not_null(const not_null& ) {}
	not_null(not_null& ) {}

	not_null( not_null&& ) {}
	not_null( const not_null&& ) {}

	not_null& operator=(const not_null& other) = default;

	constexpr T get() const
	{
		return ptr_;
	}

private:
	T ptr_;
};

int main()
{
    struct base { virtual ~base() = default ; };
    struct derived : base {};
    not_null<derived*> pd { new derived } ;
    
    // initialise not_null<base*> with not_null<derived*> 
    // types are different, so this is not copy construction
    // not const: constructor ************************ (2)
    not_null<base*> pb { pd } ;

    // initialise not_null<base*> with const not_null<derived*> 
    // types are different, so this is not copy construction
    // const: constructor ************************ (1)
    not_null<base*> pb2 { const_cast< const not_null<derived*>& >(pd) } ;
}

http://coliru.stacked-crooked.com/a/449d034582087c71
Ooooh I see... so basically

not_null(const not_null<U>& )

Is only called when we are constructing a not_null object from a not_null object of different type... and it has to be const too

1
2
3
const not_null<Derived*> pd { new Derived } ;
    
not_null<Base*> pb {pd} ; // pd both DIFFERENT TYPE and CONST 


Now I understand...

What I don't understand is why they use this kind of constructor at all?

All they do with it, is delegating it to the Universal Constructor.
Other than that, it does nothing

I mean, it's useless or not?

The Universal Constructor will do his job even without the other constructor
Last edited on
why they use this kind of constructor at all?

It's github, raise an issue and ask. For reference, the commit that introduced the pair of constructors was https://github.com/Microsoft/GSL/commit/e3fecbd1c5388a31b7d541009ed6081214458450

Oh that's nice! Thank you!

I see from the commit they just added it to avoid a call to the operator T()

The problem is, it only works if the parameter is const.

So I opened an issue about that: https://github.com/Microsoft/GSL/issues/535
Last edited on
* I apologize for opening an "old" thread *

@JLBorges With a better analysis of the code you gave me
http://coliru.stacked-crooked.com/a/36399a9a85456c23

Only now I notice you omitted the conversion operator

 
constexpr operator T() const { return get(); }


If this line is added, your code will no longer print "type-converting constructor"

So, with this in mind... how can I call the constructor

1
2
3
4
5
template <typename U, typename Dummy = std::enable_if_t<std::is_convertible<U, T>::value>>
constexpr not_null(const not_null<U>& other) : not_null(other.get())
{
	cout << "Apparently Useless Constructor" << endl;
}



http://coliru.stacked-crooked.com/a/d759a20281a9d40a
Last edited on
> If this line is added, your code will no longer print "type-converting constructor"

In http://coliru.stacked-crooked.com/a/36399a9a85456c23

a. the "universal constructor" constexpr not_null(U&& ) accepts a forwarding reference (née universal reference).
b. the "type-converting constructor" constexpr not_null(const not_null<U>& ) accpts an lvalue reference to const


> So, with this in mind... how can I call the constructor

Provide the overloads for the "type-converting constructor" such that it provides an exact match for lvalues, const lvaluesand [i]rvalues[/tt].

Note that writing it as constexpr not_null( not_null<U>&& ) won't make it a forwarding reference
(the deduced type is U, not not_null<U>)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>

template <class T> class not_null
{
    public:

        template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
        constexpr not_null(U&& u) : ptr_(std::forward<U>(u)) // universal reference
        { std::cout << "universal constructor\n" ; }

        template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
        constexpr not_null( not_null<U>& other ) : ptr_(std::forward<U>(other.get()))
        { std::cout << "type-converting constructor\n" ; }

        template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
        constexpr not_null( const not_null<U>& other ) : ptr_(std::forward<U>(other.get()))
        { std::cout << "type-converting constructor\n" ; }

        template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
        constexpr not_null( not_null<U>&& other ) : ptr_(std::forward<U>(other.get()))
        { std::cout << "type-converting constructor\n" ; }

        constexpr not_null( not_null<T>& other) : ptr_(other.get())
        { std::cout << "copy constructor\n" ; }

        constexpr not_null(const not_null<T>& other) : ptr_(other.get())
        { std::cout << "copy constructor\n" ; }

        constexpr not_null( not_null<T>&& other) : ptr_(other.get())
        { std::cout << "move constructor\n" ; }

        constexpr T get() const { return ptr_ ; }
        
        constexpr operator T() const { return get(); }

    private:
        T ptr_;
};

int main()
{
    not_null<int*> pi { new int(5) } ; // universal constructor

    not_null<int*> pi2 { pi } ; // copy constructor

    not_null<int*> pi3 { std::move(pi2) } ; // move constructor



    struct base { virtual ~base() = default ; };
    struct derived : base {};

    not_null<derived*> pd { new derived } ; // universal constructor

    not_null<derived*> pd2 { pd } ; // copy constructor

    not_null<derived*> pd3 { std::move(pd2) } ; // move constructor


    not_null<base*> pb { new base } ; // universal constructor

    // convert from derived* to base*
    not_null<base*> pb2 { pd } ; // type-converting constructor
}

http://coliru.stacked-crooked.com/a/4b4a1e7f9b25d21f
You're missing the right constructor in the code

It is a delegating constructor

1
2
3
4
5
template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
constexpr not_null( const not_null<U>& other ) : not_null(other.get())
{ 
     std::cout << "type-converting constructor\n";
}


In fact, if I add this constructor above and remove the ones that don't exist in the original version... it is never called

http://coliru.stacked-crooked.com/a/1756fca77369f22a

Last edited on
Pages: 12