Search code examples
c++templatesc++17template-argument-deductionctad

C++17 Tuple Deduction Guides (CTAD): Implicitly-Generated vs User-Defined


#include <utility>

template <class... Types>
class my_tuple {
public:
//  constexpr my_tuple() {}
//  explicit my_tuple(const Types&...){}
    template <class... UTypes> explicit my_tuple(UTypes&&...u){}

    template <class U1, class U2>
    my_tuple(const std::pair<U1, U2>& u) {}
};

// User-Defined Deduction Guides
// template<class...    UTypes>
// my_tuple(UTypes...)         -> my_tuple<UTypes...>; // #1
// template<class T1, class T2>
// my_tuple(std::pair<T1, T2>) -> my_tuple<T1, T2>;    // #2

int main(){

    int i = 1;
    double d = 4.5;

    auto mtp1 = my_tuple(i, d);         // CTAD #1

    const auto p = std::pair(i, d);
    auto mtp2 = my_tuple(p);            // CTAD #2

}

The class my_tuple tries to resemble the constructors of std::tuple. Having "enabled" only the two constructors as in the above code, CTAD works. Thus, I interpreted the implicitly-generated deduction guides are used in this case. If I uncomment (i.e., enable) one/any of the other two constructors, it still works. However, if I enable both constructors (that is, four in total), compilation fails both with GCC and CLANG (multiple overloads instantiate to the same signature). At this point, if I create (uncomment) the user-defined deduction guides (resembling the ones defined in the library for std::tuple), everything works again. I also tried implementing all the corresponding constructors as in std::tuple, and the behavior is similar.

My question is why the implicitly-generated deduction guides seem to work in the initial state of the program, but afterwards (when everything is enabled) the user-defined deduction guides are needed. My goal is to understand if the mtp1 and mtp2 object constructions (i.e., lines marked with CTAD #1 and CTAD #2) need the user-defined deduction guides or if they actually used the implicitly-generated ones.

According to CPPreference, the deduction guides from the library (corresponding to my user-defined here) are provided just to account for the edge cases missed by the implicit deduction guides, in particular, non-copyable arguments and array to pointer conversion. And the two examples from my code do not seem to me to fit in these edge cases.

Note: If I'm not mistaken, these are the implicitly-generated deduction guides from the constructors (plus the additional copy deduction candidate [over.match.class.deduct], paragraph 1.3) in the original state of my example code:

template<class...    UTypes>
my_tuple(UTypes&&...) -> my_tuple<UTypes&&...>; // #1

template<class U1, class U2>
my_tuple(const std::pair<U1, U2>&) -> my_tuple<const std::pair<U1, U2>&>; // #2

template <typename T>
my_tuple(my_tuple<T>) -> my_tuple<T> // #3 

Interesting links:


Solution

  • You might check the resulting types, it would surprise you Demo

    my_tuple(i, d) results in my_tuple<>.

    And once extra constructors un-commented, you have two default constructors for my_tuple<>

    constexpr my_tuple() {}
    explicit my_tuple(const Types&...){} /* with empty pack Types */
    

    resulting in error.

    With CTAD activated, you no longer generate problematic my_tuple<> (which is still problematic BTW).