Why is my "Universal" Constructor called twice?

Pages: 12
> it is never called

The "type-converting constructor" as written above provides the best match only for
an lvalue of type const not_null<U>; when it provides the best match, it will be the one that is called.

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
#include <iostream>
#include <typeinfo>

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( const not_null<U>& other )
            : not_null{ ( (std::cout << "\n(1). type-converting constructor: delegate to universal constructor => "), other.get() ) }
        { std::cout << "(2). type converting constructor: done\n" ; }

        constexpr T get() const { return ptr_ ; }

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

    private:
        T ptr_;
};

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

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

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

universal constructor

(1). type-converting constructor: delegate to universal constructor => universal constructor
(2). type converting constructor: done

http://coliru.stacked-crooked.com/a/9884f592d05cbcbe
Last edited on
Thank you very much!
If there is no operator T() explicit conversion, the best match for the constructor is type-converting constructor

If there is operator T() explicit conversion, the type-converting constructor is ignored and the conversion is performed. After the conversion, the Universal constructor is choosen.

WHY is the const qualifier taken into account when operator T() isn't available... while it's ignored when operator T() is defined ?

http://coliru.stacked-crooked.com/a/6a55e3178d76a0cd

Try commenting and un-commenting the operator T()
Last edited on
I have played around with this for a while, and this is what I see
(the delegations in the constructors have been removed to eliminate white noise):

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
#include <iostream>
#include <typeinfo>

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&&) : ptr_(nullptr)
        { std::cout << "universal constructor: U == " << typeid(U).name() << '\n' ; }

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

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

        #ifdef OPERATOR_T
            constexpr operator T() const { std::cout << "converting...\n"; return get(); }
        #endif // OPERATOR_T

        constexpr T get() const { return ptr_ ; }

    private: T ptr_;
};

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

int main()
{
    std::cout << "not_null<derived*> pd { new derived } : " ;
    not_null<derived*> pd { new derived } ;

    std::cout << "             not_null<base*> pb2 {pd} : " ;
    not_null<base*> pb2 {pd} ;
}


LLVM:
=============== LLVM ========================
without operator T()
not_null<derived*> pd { new derived } : universal constructor: U == P7derived
             not_null<base*> pb2 {pd} : type-converting constructor: not_null<U> == 8not_nullIP7derivedE>

with operator T()
not_null<derived*> pd { new derived } : universal constructor: U == P7derived
             not_null<base*> pb2 {pd} : universal constructor: U == 8not_nullIP7derivedE


GNU:
===============  GNU ========================
without operator T()
not_null<derived*> pd { new derived } : universal constructor: U == P7derived
             not_null<base*> pb2 {pd} : type-converting constructor: not_null<U> == 8not_nullIP7derivedE>

with operator T()
not_null<derived*> pd { new derived } : universal constructor: U == P7derived
             not_null<base*> pb2 {pd} : universal constructor: U == 8not_nullIP7derivedE


http://coliru.stacked-crooked.com/a/08057d1838faba2b

Microsoft:
without operator T()
not_null<derived*> pd { new derived } : universal constructor: U == struct derived * __ptr64
             not_null<base*> pb2 {pd} : type-converting constructor: not_null<U> == class not_null<struct derived * __ptr64>>

with operator T()
not_null<derived*> pd { new derived } : universal constructor: U == struct derived * __ptr64
             not_null<base*> pb2 {pd} : universal constructor: U == class not_null<struct derived * __ptr64>


http://rextester.com/LIJFJ46520
http://rextester.com/JXN99671

All three mainstream compilers behave in an identical manner; with an accessible operator T() present, the resolution of the overload gives a different result, even though the operator plays no part in the eventual call.

a. Is this the correct behaviour as specified by the standard?
(It is likely that it is, since all three compilers interpret it in the same way)

If the answer to a. is yes:
b. What part does the presence or absence of an accessible operator T() play in resolving the overload?
c. If it is involved in overload resolution, why is it not called?
d. If the standard does specify that this is the expected behaviour, is there a defect in the standard?

I have some vague, hough somewhat strong, hunches; but I do not have enough knowledge to be able to give a definitive answer.

I'm sending a PM to Cubbi requesting him to have a look at this, and enlighten us.
JLBorges wrote:
I'm sending a PM to Cubbi requesting him to have a look at this


I added an ambiguous constructor to take a look at the set of candidate functions selected in each case (gcc's output is more detailed here than clang's) - removing my added constructor in each case, the sets were:

without OPERATOR_T

note: candidate: 'constexpr not_null<T>::not_null(const not_null<U>&) [with U = derived*; Dummy = void; T = base*]'
         constexpr not_null(const not_null<U>&) : ptr_(nullptr)
note: candidate: 'constexpr not_null<base*>::not_null(const not_null<base*>&)'
note: candidate: 'constexpr not_null<base*>::not_null(not_null<base*>&&)'


with OPERATOR_T

note: candidate: 'constexpr not_null<T>::not_null(U&&) [with U = not_null<derived*>&; Dummy = void; T = base*]'
         constexpr not_null(U&&) : ptr_(nullptr)
note: candidate: 'constexpr not_null<T>::not_null(const not_null<U>&) [with U = derived*; Dummy = void; T = base*]'
         constexpr not_null(const not_null<U>&) : ptr_(nullptr)
note: candidate: 'constexpr not_null<base*>::not_null(const not_null<base*>&)'
note: candidate: 'constexpr not_null<base*>::not_null(not_null<base*>&&)'


The difference is that "universal constructor" becomes a candidate when operator T is added. And then it takes over overload resolution as usual (those constructors are infamously "greedy")

Without operator T, is_convertible<U,T> is false [with U = not_null<derived*>&; T = base*] and enable_if simply disables that constructor. (that last bit was observed by commenting out the enable_if and moving its condition into a static_assert inside the constructor)
Last edited on
> Without operator T, is_convertible<U,T> is false [with U = not_null<derived*>&; T = base*]
> and enable_if simply disables that constructor.

Right.

Thank you very much!
Thank you both for taking a look at this

Without operator T, is_convertible<U,T> is false and enable_if simply disables that constructor

This isn't clear to me.

When is_convertible<U,T> is false, BOTH constructors should be disabled, because both of them have the enable_if
Last edited on
> When is_convertible<U,T> is false, BOTH constructors should be disabled,
> because both of them have the enable_if

In our example:

for the universal constructor, U == not_null<derived*>
There is an implicit conversion from not_null<derived*> to base* only if there is an accessible operator T()

For the type-converting constructor, U == derived*
The implicit conversion is the standard pointer-to-derived-class to pointer-to-unambiguous-base-class conversion
This is so subtle!

Thank you very much... I think I'll get away from this code for a bit :D
Topic archived. No new replies allowed.
Pages: 12