c++ template type deduction fail in cast operator
Asked Answered
S

2

8

I have simplified my somewhat more difficult problem to this:

http://coliru.stacked-crooked.com/a/2660b33492651e92

#include <iostream>
#include <string>
#include <type_traits>

template<typename C>
struct get_type
{
    C operator()() const = delete;
};

template<>
struct get_type<std::string>
{
    std::string operator()() const { return "asd"; }
};

template<>
struct get_type<size_t> {
    size_t operator()() const { return 6; }
};

struct S
{
    S(){}
    template<typename T>
    operator T() { return get_type<T>{}(); }
};

struct A
{
    A() :s{S{}}, n{S{}} {}
    std::string s;
    size_t n;
};

int main()
{
    A a;
    std::cout << "Spock out." << std::endl;
}

This generates the following error:

'In instantiation of 'S::operator T() [with T = char]':'...

Why is T is deduced to char and not std::string?

Edit:

@YSC's answer seems to be correct: https://mcmap.net/q/1305935/-c-template-type-deduction-fail-in-cast-operator

I edited the post to add a solution: http://coliru.stacked-crooked.com/a/06d31d981acd2544

struct S
{
    S(){}
    template<typename T>
    explicit operator T() { return get_type<T>{}(); }
};
Senzer answered 6/10, 2017 at 15:5 Comment(0)
G
6

Here:

A() : s( S{} ), ...

the construction of A::s from a S instance is ambiguous because for every type T for witch std::is_constructible<std::string, T>, the template function S::operator T() is a possible conversion path from S to T to std::string.

It seams your version of GCC starts by testing with T = char. clang list multiple candidates: http://coliru.stacked-crooked.com/a/17e247cca8b79c77:

/usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../include/c++/7.2.0/bits/basic_string.h:413:7: note: candidate constructor
      basic_string(const _Alloc& __a) _GLIBCXX_NOEXCEPT
      ^
/usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../include/c++/7.2.0/bits/basic_string.h:421:7: note: candidate constructor
      basic_string(const basic_string& __str)
      ^
/usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../include/c++/7.2.0/bits/basic_string.h:493:7: note: candidate constructor
      basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
      ^
/usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../include/c++/7.2.0/bits/basic_string.h:515:7: note: candidate constructor
      basic_string(basic_string&& __str) noexcept
      ^
/usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../include/c++/7.2.0/bits/basic_string.h:542:7: note: candidate constructor
      basic_string(initializer_list<_CharT> __l, const _Alloc& __a = _Alloc())

As OP found by themself, making the conversion operator of S explicit solves the ambiguity (demo):

struct S
{
    S(){}
    template<typename T>
    explicit operator T() { return get_type<T>{}(); }
};

And this works (as user AndyG found) because under [over.match.copy] one can read:

When initializing a temporary to be bound to the first parameter of a constructor where the parameter is of type “reference to possibly cv-qualified T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv2 T”, explicit conversion functions are also considered.

Gorden answered 6/10, 2017 at 15:11 Comment(8)
If you replace return get_type<T>{}(); with return {};, GCC actually compiles the code. I'm guessing GCC makes the list constructor greedy in this context and Clang doesn't.Bartolemo
@Bartolemo looks like a gcc misbehaviour.Gorden
Oh, I also just realized the Coliru link has : s(...) and the question has : s{...} :( Clang also compiles it with my above change if you use : s{...}.Bartolemo
omg you are right coliru.stacked-crooked.com/a/06d31d981acd2544 if I make the operator T explicit, it compilesSenzer
Neat. I wondered why an explicit conversion operator was considered at all, but it's right there in the standard under [over.match.copy]: When initializing a temporary to be bound to the first parameter of a constructor where the parameter is of type “reference to possibly cv-qualified T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv2 T”, explicit conversion functions are also considered. Edit: or perhaps the similar quote under [over.match.conv] is better.Fulsome
@Fulsome citation addedGorden
@Bartolemo Looks like the coliru link was updated to be consistent with the question, both using s{...} now.Unyoke
@Fulsome [over.match.conv] only discusses initialization of objects of non-class type.Unyoke
U
4

Here:

A() : s { S{} }, ...

You are initializing s with a braced-init-list, and std::string has a constructor from std::initializer_list<char>, so this is considered first. This succeeds, resulting in a call to S::operator char, which is ill-formed because it contains a call to a deleted function.

When you add the explicit keyword to the conversion function, this makes the constructor from initializer_list non-viable, so the compiler must then consider the other constructors. Of those, the copy constructor is the only viable one, for the reason given by @YSC: explicit conversion functions are allowed when considering a copy constructor in the context of direct-initialization.

Unyoke answered 6/10, 2017 at 21:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.