35

program:

#include <stdio.h>

struct bar_t {
    int value;
    template<typename T>
    bar_t (const T& t) : value { t } {}

    // edit: You can uncomment these if your compiler supports
    //       guaranteed copy elision (c++17). Either way, it 
    //       doesn't affect the output.

    // bar_t () = delete;
    // bar_t (bar_t&&) = delete;
    // bar_t (const bar_t&) = delete;
    // bar_t& operator = (bar_t&&) = delete;
    // bar_t& operator = (const bar_t&) = delete;
};

struct foo_t {
    operator int   () const { return 1; }
    operator bar_t () const { return 2; }
};

int main ()
{
    foo_t foo {};
    bar_t a { foo };
    bar_t b = static_cast<bar_t>(foo);

    printf("%d,%d\n", a.value, b.value);
}

output for gcc 7/8:

2,2

output for clang 4/5 (also for gcc 6.3)

1,1

It seems that the following is happening when creating the instances of bar_t:

For gcc, it calls foo_t::operator bar_t then constructs bar_t with T = int.

For clang, it constructs bar_t with T = foo_t then calls foo_t::operator int

Which compiler is correct here? (or maybe they are both correct if this is some form of undefined behaviour)

verb_noun
  • 375
  • 5
  • 11
  • To me, it seems that since `bar_t` will still have its default constructor in the form `bar_t(const bar_t&)`, and since regular functions beat template functions as matches all things being equal, the correct conversion sequence in both cases is that followed by gcc 7,8. I don't see any possible way that clang 4/5 can be right; ambiguous at best. – Nir Friedman May 18 '17 at 22:24
  • @NirFriedman I just did a quick test with the `default constructors` deleted from `bar_t` and I'm still getting the same results as the original post. – verb_noun May 18 '17 at 22:44
  • @NirFriedman but it seems like this copy ctor is deleted. `bar_t (const bar_t&) = delete;` – Kobi May 18 '17 at 23:21
  • Btw, Visual C++ 2017 fails to compile the construction of `b`, it seems to be preferring the move ctor over the templated ctor and complains that it is deleted. – isanae May 18 '17 at 23:25
  • @Kobi I added that line after NirFriedman commented. – verb_noun May 18 '17 at 23:25
  • @isanae I don't have MSVC++ to test it, but I think [copy elision](http://en.cppreference.com/w/cpp/language/copy_elision) should apply in that case and eliminate the move ctor. I don't know why MSVC++ is not doing that. – verb_noun May 18 '17 at 23:30
  • 2
    @isanae As of C++17, the constructors are not required in cases where copy elision is mandatory. – Oktalist May 18 '17 at 23:54
  • 2
    Uhm, if you can't say up front what the effect of construct is, just don't use it. And with three compilers all disagreeing with each other about it, flee for your life. ;-) – Cheers and hth. - Alf May 19 '17 at 00:05
  • 1
    MSVC doesn't implement C++17 guaranteed elision. Clang is correct. – T.C. May 19 '17 at 00:13
  • I think this is a bug in both compilers and this code should be an error (and indeed [Clang 3.5.0 does not compile this](https://wandbox.org/permlink/cuWg0DJVcoiWjYoL)). The compiler should pick the `bar_t` conversion of `foo_t` and then notice that the copy constructor has been deleted. – Henri Menke May 19 '17 at 00:24
  • @HenriMenke Enabling C++17 avoids that error via C++17 guaranteed copy elision (though not in clang 3.5 since this C++17 feature was only implemented in version 4). – bames53 May 19 '17 at 00:35
  • 1
    @T.C. It does implement it in the latest (3) update to VS2017: https://blogs.msdn.microsoft.com/vcblog/2017/05/10/c17-features-in-vs-2017-3/ – Dan M. May 19 '17 at 02:02

1 Answers1

18

I believe clang's result is correct.

In both bar_t a { foo } direct-list-initialization and in a static_cast between user defined types, constructors of the destination type are considered before user defined conversion operators on the source type (C++14 [dcl.init.list]/3 [expr.static.cast]/4). If overload resolution finds a suitable constructor then it's used.

When doing overload resolution bar_t::bar_t<foo_t>(const foo_t&) is viable and will be a better match than one to any instantiation of this template resulting in the use of the cast operators on foo. It will also be better than any default declared constructors since they take something other than foo_t, so bar_t::bar_t<foo_t> is used.


The code as currently written depends on C++17 guaranteed copy elision; If you compile without C++17's guaranteed copy elision (e.g. -std=c++14) then clang does reject this code due to the copy-initialization in bar_t b = static_cast<bar_t>(foo);.

bames53
  • 86,085
  • 15
  • 179
  • 244
  • 2
    Looks like this is a bug in the newer versions of gcc (7+). Older versions (6.3.1) are consistent with clang. I gotta say though, I think I prefer the bugged version as it gives an extra configuration point (conversion operators) to control how one type is constructed from another. – verb_noun May 19 '17 at 13:03