4

I have the two functions that are almost the same (with the exception that one of them is a template):

int* bar(const std::variant<int*, std::tuple<float, double>>& t)
{
    return std::get<0>(t);
}
template <typename... Args>
int* foo(const std::variant<int*, std::tuple<Args...>>& t)
{
    return std::get<0>(t);
}

Than, they are use like this:

foo(nullptr);
bar(nullptr);

The second one compiles and returns (int*)nullptr, but the first one doesn't (in Visual Studio 2019 using C++17 giving the error foo: no matching overload found). Why? Why does making this function a template cause it to cease to compile?

Using foo like below doesn't help either, so the inability to deduce Args is probably not the problem:

foo<>(nullptr);

In contrary, the following does work:

foo(std::variant<int*, std::tuple<>>(nullptr));

Is it possible to somehow avoid the need to write this in such a long manner?

  • 2
    Templates don't do conversions. `nullptr` is not a `variant<...>` – Barry Jul 11 '19 at 17:53
  • @Barry Do you mean that the standard says that in function templates implicit conversions are forbidden? –  Jul 11 '19 at 17:58
  • Templates attempt to match the types you provide. They do not try to expand into something else compatible with accepting your argument. In this case, even if it did allow such conversions, calling your template with nullptr would still fail because it would be impossible to deduce what type(s) _Args..._ should contain. – Chris Uzdavinis Jul 11 '19 at 18:00
  • @ChrisUzdavinis `foo<>(nullptr);` doesn't work either, although `Args` is known. –  Jul 11 '19 at 18:02
  • @YanB. Apparently it's not known in this case. `foo<>` means that there are [*at least*](http://coliru.stacked-crooked.com/a/e86d121ad9e4481c) 0 types in `Args`, which doesn't mean anything. – HolyBlackCat Jul 11 '19 at 18:27

3 Answers3

3

Apparently if a type of a function parameter depends on a template parameter that has to be deduced (because it's not specified in <...>), then implicit conversions don't apply when passing an argument to that parameter.

Source:

The function parameters that do not participate in template argument deduction (e.g. if the corresponding template arguments are explicitly specified) are subject to implicit conversions to the type of the corresponding function parameter (as in the usual overload resolution).

A template parameter pack that is explicitly specified may be extended by template argument deduction if there are additional arguments:

template<class ... Types> void f(Types ... values);
void g() {
  f<int*, float*>(0, 0, 0); // Types = {int*, float*, int}
}

This also explains why foo<>(nullptr); still doesn't work. Since the compiler tries to deduce additional types to extend Args, in this case there doesn't seem to be any difference between foo(nullptr); and foo<>(nullptr);.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • I still don't quite understand this - it says that an explicitly specified template parameter pack *may* be extended by template argument deduction, but naively I would have expected that if this extension deduction fails, the compiler should just take the explicitly specified arguments as being the entire pack. Instead, in the OP's code, we saw that the *enclosing* deduction/substitution fails. Do you know where in the standard this behaviour is specified? – Brian Bi Jul 11 '19 at 20:39
  • @Brian I don't know what part of the standard describes it, but this behavior seems to match the cppreference quote. Since the parameter participates in the template argument deduction, implicit conversions aren't allowed when passing an argument to this parameter. – HolyBlackCat Jul 11 '19 at 20:52
  • Maybe I wasn't clear about why I was confused - obviously, the part of the deduction that attempts to extend the parameter pack cannot succeed because user-defined conversions are not considered. But, given that the deduction fails, why doesn't the compiler just say: "well, since extending the parameter pack failed, I'll just assume that the explicitly specified arguments constitute the entire pack"? – Brian Bi Jul 11 '19 at 21:36
  • @Brian ¯\\_(ツ)_/¯ I don't see a good reason for that too. It's probably just a one of many C++ quirks. – HolyBlackCat Jul 11 '19 at 21:42
1

When a template function is considered it will only work for an exact match of the argument types at the call. This means that no conversions will be made (except for cv qualifiers).

A simple workaround in your case would be to make a function catch std::nullptr_t and forward that to your template.

int* foo(std::nullptr_t) {
    return foo(std::variant<int*, std::tuple<>>{nullptr});
}
super
  • 12,335
  • 2
  • 19
  • 29
  • Thanks for pointing this out, but unfortunately this only work with the pointer side of the `std::variant`, but not with `std::tuple`. I tried using `template int* foo(std::tuple tpl)`, but when using it like this: `foo({ 5, 7.0f, 5.0 });` it doesn't work. I guess that its because `decltype({ 5, 7.0f, 5.0 })`(whatever that is) is not `std::tuple`. –  Jul 11 '19 at 18:25
  • @YanB. Yes, but that isn't the question you asked in your question. Please press the "ask a question" button with your new question, rather than asking it in comments. – Yakk - Adam Nevraumont Jul 11 '19 at 18:30
0

I would avoid this construct simply because the rules about exactly how the compiler will (if it even does it at all) resolve the overload are so confusing that I couldn't really tell you what it did without looking at a standards document, and code like that should be avoided. I would force the overload resolution you want this way instead:

template <typename... Args>
int *foo(const ::std::variant<int*, ::std::tuple<Args...>> &t)
{
    return ::std::get<0>(t);
}

int *foo(int *ip)
{
    using my_variant = ::std::variant<int *, ::std::tuple<>>;
    return foo(my_variant{ip});
}

template <typename... Args>
int *foo(::std::tuple<Args...> const &t)
{
    using my_variant = ::std::variant<int *, ::std::tuple<Args...>>;
    return foo(my_variant{t});
}

template <typename... Args>
int *foo(::std::tuple<Args...> &&t)
{
    using my_variant = ::std::variant<int *, ::std::tuple<Args...>>;
    return foo(my_variant{::std::move(t)});
}
Omnifarious
  • 54,333
  • 19
  • 131
  • 194
  • That's just what I was thinking about. Thanks! BTW, why do you precede all `std` members with the scope operator (apart from `std::get`)? –  Jul 11 '19 at 18:45
  • @YanB. - I refer you to this SO question... https://stackoverflow.com/questions/1661912/why-does-everybody-use-unanchored-namespace-declarations-i-e-std-not-std - I cut & pasted, and forgot to change a bunch of the original code. I'll fix that now. :-) – Omnifarious Jul 11 '19 at 18:47