Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Templated conversion operator priority

Tags:

c++

c++17

Some C++ hacks use the conversion operator the get some information about the constructor. I would like to know, what is the process when choosing a concrete type for T in the resolution of a templated cast operator.

#include <iostream>
#include <type_traits>

using std::cout;
using std::endl;

struct A {
    A(int) { cout << "int" << endl; }
    A() { cout << "def" << endl; }
    A(const A&) { cout << "copy" << endl; }
    A(A&&) { cout << "move" << endl; }
};

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

template<typename Except>
struct C {
    template<typename T,
             std::enable_if_t<!std::is_same_v<T, Except>>* = nullptr> operator T()
        { return {}; }
};

template<typename T>
void f(A a = { T() }) {}

int main() {
    f<B>();
    f<C<A>>();

    return 0;
}

This code print this:

def
int

And not this:

int
int

Why should I disable conversion for taking the constructor I want (int version)? The C++ standard says that the return type don't participate in finding a valid template overload, so why it choose this version without complaining about multiple possible resolutions ?

Makefile:

EXE = C++Tuple
CXX = g++
CXXFLAGS = -std=c++17

run: $(EXE)
    ./$(EXE)
.PHONY: run

$(EXE): main.cpp
    $(CXX) $(CXXFLAGS) -o $@ $<

Plateform:

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.5.0-3ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
$ uname -r
4.4.0-19041-Microsoft
like image 352
rafoo Avatar asked Sep 14 '20 21:09

rafoo


1 Answers

First, several compilers/versions, including current GCC, do reject this as written.

Copy initialization

Let's start with a simpler case:

template<typename T>
void f(A = T()) {}

Here, we have an expression of type T that we want to (implicitly) convert to A. Unsurprisingly, B produces operator A as a specialization of its conversion function template, and C produces nothing because of SFINAE.

Note that the "expected" return type of a conversion function template definitely contributes to template argument deduction (since otherwise it would be impossible to ever deduce anything for them!). Moreover, T is never deduced as A&& or so, even though that would be allowed by the constraint; only the singular, "obvious" type is used for such deduction, even though certain other types (e.g., a derived class) are allowed for (non-template) conversion functions (for which no effort needs to expended per allowed type).

Clang produces a confusing error message here, saying that it can't convert C<A> to int in attempting to call the A(int) constructor; that conversion is of course possible in general, but is disallowed in this case by the usual rule about multiple user-defined conversions in [over.best.ics.general]/4:

However, if the target is the first parameter of a constructor [...] and the constructor or user-defined conversion function is a candidate by [...], [over.match.copy], or [...] user-defined conversion sequences are not considered.

List initialization

However, the multiple-conversions rule doesn't apply to list initialization in general (because you perform normal overload resolution, allowing conversions, inside each layer of braces). As such, (user-defined) conversions from the argument to the parameter type for each constructor of A are considered.

B

Current versions of GCC, ICC, and MSVC all reject this for ambiguity, since B can be converted to int or to A. (ICC helpfully points out that the move constructor is a better match than the copy constructor because of [over.ics.rank]/3.2.3, but there are still two choices.) It's hard to guess why Clang ignores the former possibility (what with no diagnostic output from it), but the other compilers appear to be correct: [dcl.init.list]/3.7 defers to normal overload resolution (except for preferring std::initializer_list constructors, which aren't relevant here), and there is no reason to prefer one constructor over the other (since a user-defined conversion sequence followed by an exact-match standard conversion sequence is involved in each case).

C<A>

Again, the deduction for the const A& or A&& constructors picks T=A (this time because of [temp.deduct.conv]'s simplifications rather than [over.match.copy]'s restrictions) and finds nothing. Therefore only the "conversion" C<A>intA works. All four compilers agree about this case, although MSVC erroneously issues a warning about an "illegal" double conversion.

like image 55
Davis Herring Avatar answered Oct 16 '22 08:10

Davis Herring