11

This code fails because the compiler thinks template arguments for Test is an empty set.

#include <tuple>

template <typename... T>
class Test
{
public:
    template <typename Arg, typename... Args>
    Test(Arg&& arg1, Args&&... args) : tuple_{std::forward<Arg>(arg1), std::forward<Args>(args)...} 
    {}

private:
    std::tuple<T...> tuple_;
};


template <typename... Ts>
Test(Ts...) -> Test<Ts...>;

int main()
{
    Test t{1, 2};
}

why is that? Live example

Changing deduction guide to "fully match" the constructor works on the other hand

4
  • It is because your deduction guide does not match the template arguments of your constructor. Your deduction guide only has the pack, not the first T. See : temp.deduct.guide which refers to parameter-declaration-clause Commented yesterday
  • Possibly relevant question: stackoverflow.com/q/43430921/580083 Commented yesterday
  • @Jarod42 yea, and it looks weird, If we forbid an empty version it compiles OK - godbolt.org/z/xeoGMedcP Commented yesterday
  • @PepijnKramer Deduction guides do not need to match a constructor. Commented yesterday

3 Answers 3

11

From cppinsight you might see the auto-generated deduction guide as well (comment mine):

template<typename... Ts, typename Arg, typename... Args>
Test(Arg&& arg1, Args&&... args) -> Test<Ts...>; // Empty Ts as Ts not deducible

And it is a better match than your explicit deduction guide for Test t{1, 2};

template <typename... Ts> Test(Ts...) -> Test<Ts...>;

Several solutions:

  • Rewrite the explicit deduction guide to be a better match:
template <typename T, typename... Ts> Test(T, Ts...) -> Test<T, Ts...>;

There is a tie-breaker explicit versus implicit.

  • Add constraints, so the constructor (implicit deduction guide) can be SFINAEd out. (requires (sizeof(T) != 0) on class/constructor, or std::is_constructible on constructor)
Sign up to request clarification or add additional context in comments.

2 Comments

Image
If I get it correctly. The compiler generated its own deduction guide and assumes it's a better match because the custom one is the most general possible (the least specialized that can theoretically be). Well the question is why does compiler generate auto deduction guide if there is a user-provided one?
User-provided deduction guides do not prevent the compiler from generating implicit ones from the constructor. Have a look at en.cppreference.com/w/cpp/language/deduction_guide.html , where you can see that the rules about "implicitly-generated deduction guides" has nothing to do with user-provided ones. When selecting the guide to adopt, all these guides "compete" following the rules of overload resolution, and that overload resolution is independent from the overload resolution of the constructors.
2

A possible solution is to constrain your constructor:

#include <tuple>
#include <concepts>

template <typename... T>
class Test
{
public:
    template <typename Arg, typename... Args>
    requires (requires {
        std::tuple<T...>{std::declval<Arg>(), std::declval<Args>()...};
    })
    Test(Arg&& arg1, Args&&... args) : tuple_{std::forward<Arg>(arg1), std::forward<Args>(args)...} 
    {}

private:
    std::tuple<T...> tuple_;
};


template <typename... Ts>
Test(Ts...) -> Test<Ts...>;

int main()
{
    Test t{1, 2};
}

This makes the implicitly generated deduction guide not usable because std::tuple<> cannot be initialized with two ints.

godbolt demo

Note that cppinsights does not show the constraints for the implicitly generated deduction guides.

Comments

1

Based on the cppinsights answer, a possible workaround would be to remove type deduction in the constructor itself, https://godbolt.org/z/Ksarv1Knd

#include <tuple>

template <typename... T>
class Test {
   public:
    Test(T&&... args) : tuple_{std::move(args)...} {}
    Test(T const&... args) : tuple_{args...} {}

   private:
    std::tuple<T...> tuple_;
};

template <typename... Ts>
Test(Ts...) -> Test<Ts...>;

int main() { Test t{1, 2}; }

Although I am now not entirelly sure if you need the const and non-const version, or if it makes any difference.

UPDATE: https://godbolt.org/z/36qzT6sfz

2 Comments

Image
"if it makes any difference." Issue here is that T&& is not a forwarding reference and actually a rvalue reference, and so each T should have same category (for 1, 2 it is not an issue, but if you mix copyable and movable-only types, it might be problematic (having both doesn't resolve the issue).
I know it is not a forwarding reference; it is there to handle rvalues a bit more generally than the alternative Test(T... args) : tuple_{std::move(args)...} {}, but as you say, mixing value categories would be problematic. I am leaning toward a single constructor explicit Test(T... args) : tuple_{std::move(args)...} {},

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.