23

I'm trying to port some code written for GCC (8.2) to be compilable by Clang:

#include <tuple>

struct Q{};

using TUP = std::tuple<Q>;


template<typename Fn>
inline
void feh(Fn&, const std::tuple<>*)
{}

template<typename Fn, typename H>
inline
void feh(Fn& fn, const std::tuple<H>*)
{
    fn(H{});
}

template<typename  Fn, typename H, typename... R>
inline
void feh(Fn& fn, const std::tuple<H, R...>*)
{
    fn(H{});
    using Rest = const std::tuple<R...>*;
    feh<Fn, R...>(fn, static_cast<Rest>(nullptr));
}

template<typename Tuple, typename Fn>
inline
void fe(Fn& fn, const Tuple  *  tpl =  nullptr)
{
    feh(fn, tpl);
}

int main()
{
    auto r = [] (Q const&) {};
    TUP tup;
    fe<TUP>(r, &tup);
}

GCC 8.2 (and 12.1) compiles the code just fine. However, Clang 11.0.0 (and 14.0.0) complains that the call from fe to feh is ambiguous between void feh(Fn& fn, const std::tuple<H>*) [with Fn = (lambda at <source>:38:14), H = Q] and void feh(Fn& fn, const std::tuple<H, R...>*) [with Fn = (lambda at <source>:38:14), H = Q, R = <>].

https://godbolt.org/z/5E9M6a5c6

Which compiler is right?

How can I write this code so both compilers accept it?

Both if constexpr and fold expressions would work in C++17, but this is a library header included by many projects, and not all of them are compiled with C++17. I need a solution which works in C++11.

cigien
  • 57,834
  • 11
  • 73
  • 112
Bulletmagnet
  • 5,665
  • 2
  • 26
  • 56
  • 1
    Which standard is targeted? `template void fe(Fn& fn, const std::tuple* = nullptr) { (fn(Ts{}), ...); }` should be fine for c++17 [Demo](https://godbolt.org/z/W44vfcTGG). – Jarod42 Jun 17 '22 at 11:29
  • This code is currently compiled with `-std=c++17` but it was written before C++17 was available. – Bulletmagnet Jun 17 '22 at 11:52
  • Probably should add the `language-lawyer` tag for this pickle of a situation. – Eljay Jun 17 '22 at 13:43
  • 1
    @Bulletmagnet Since you've added the C++11 requirement, I added the C++11 tag. Perhaps you should update your Compiler Explorer example to use C++11 instead of C++17 too? – Ted Lyngmo Jun 17 '22 at 13:48
  • 1
    Just drop the overload with only H and let the template arguments be reduced when you call feh, your code would be equivalent and no more possibly conflicting overload. – Holt Jun 17 '22 at 20:27

4 Answers4

17

Which compiler is right?

Clang is wrong in rejecting the code because the first overload candidate feh(Fn& fn, const std::tuple<H>*) should be preferred over the other candidate feh(Fn& fn, const std::tuple<H, R...>*) since the former is more specialized than the latter.

In other words, the version without the pack is considered more specialized and hence should be preferred if it matches the call.


This is because, basically(roughly) for one function template to be considered more specialized than the other, the latter should be able to accept all the template arguments that the former can accept but not vice-versa.

Now, in your given example the overload feh(Fn& fn, const std::tuple<H, R...>*) can accept(or work with) all template arguments which the former feh(Fn& fn, const std::tuple<H>*) can accept but the reverse is not true. Hence the former is more specialized than the latter. For more technical details of this process, refer to What is the partial ordering procedure in template deduction or from [temp.deduct.partial]/10 which states:

Function template F is at least as specialized as function template G if, for each pair of types used to determine the ordering, the type from F is at least as specialized as the type from G. F is more specialized than G if F is at least as specialized as G and G is not at least as specialized as F.

(emphasis mine)

Jason
  • 36,170
  • 5
  • 26
  • 60
  • Could this be CWG issue 692 , which clang doesn't implement yet ? – Bulletmagnet Jun 17 '22 at 14:42
  • @Bulletmagnet [CWG 692](https://cplusplus.github.io/CWG/issues/692.html) talks about extending [temp.deduct.type](https://wg21.link/temp.deduct.type) for partial ordering of class template specialization. But note that in our current example/question we're talking about partial ordering of function templates. Thus, they are different issues as cwg 692 also notes that [temp.deduct.type] is already about function templates. – Jason Jun 17 '22 at 14:51
  • And clang seems to implement CWG 692: – Bulletmagnet Jun 17 '22 at 14:53
  • @Bulletmagnet Yes, i was about to point out this. But it looks like you already got the point. Also did you report this as a [bug](https://llvm.org/docs/HowToSubmitABug.html) on their official site? They will provide a patch and you will no longer have any issue with your program and also you'll no longer need to change anything in your current program. – Jason Jun 17 '22 at 14:55
  • https://github.com/llvm/llvm-project/issues/56090 Unfortunately, I need to get my program to compile now, and not when clang 15, or 16, or higher, is released. – Bulletmagnet Jun 17 '22 at 15:11
  • _CWG 692 talks about extending temp.deduct.type for partial ordering of class template specialization. But note that in our current example/question we're talking about partial ordering of function templates_ CWG 692 talks about extending temp.deduct.type for partial ordering based on not only function, but also template parameter pack. This can be used both to order template specializations and function templates. – Language Lawyer Jun 17 '22 at 18:45
  • @LanguageLawyer Yes that's what i mean in that comment. Maybe i was not able to write/describe it as clear as i wanted to. – Jason Jun 17 '22 at 18:46
  • _For more technical details of this process, refer to What is the partial ordering procedure in template deduction or from function template overloading which states: In case of a tie, if one function template has a trailing parameter pack and the other does not_ Are you sure you understand what is a function with trailing parameter pack? – Language Lawyer Jun 17 '22 at 18:50
  • @LanguageLawyer Are you saying that the quote *"In case of a tie, if one function template has a trailing parameter pack and the other does not"* is not useful here since `const std::tuple*` is not a function parameter pack? – Jason Jun 17 '22 at 18:53
  • The source you are citing even has an example for the tie-breaker… – Language Lawyer Jun 17 '22 at 18:54
  • @Bulletmagnet _clang seems to implement CWG 692: _ Not completely, try an example added in http://wg21.link/n3281 (the resolution of CWG692) – Language Lawyer Jun 17 '22 at 19:25
  • Anoop, sorry for butting in in your comment field, but it feels like you two are discussion what I was wobbly about when I answered. @LanguageLawyer If the exact phrasing is disregarded, is Anoop correct? There should not have been ambiguity in this selection? I was wobbly when answering myself because I got the feeling that the resolution for the template arguments to the _one_ argument both functions have would fall under another, later, deduction, and that it'd then be too late - but that was only thoughts. I have no foundation for it. – Ted Lyngmo Jun 17 '22 at 23:14
  • @TedLyngmo _If the exact phrasing is disregarded_ It can't be. _is Anoop correct?_ If I understand https://timsong-cpp.github.io/cppwp/n4861/temp.deduct.type#9.1 correctly, when comparing `` against ``, `R...` is discarded/ignored. And then functions are indistinguishable. Unless there is a tie-breaker. – Language Lawyer Jun 18 '22 at 11:26
  • @LanguageLawyer Ok, and as you see it, there is no tie-breaker here? – Ted Lyngmo Jun 18 '22 at 11:29
  • @TedLyngmo https://cplusplus.github.io/CWG/issues/1395.html added a tie-breaker only for trailing function parameter packs – Language Lawyer Jun 18 '22 at 15:21
  • @LanguageLawyer Cool, thanks! I guess my initial hunch was correct then. :-) – Ted Lyngmo Jun 18 '22 at 15:25
11

clang++ is correct because both functions matches equally good. I'm unsure which compiler that is correct, but...

A C++11 solution could be to just add the requirement that the Rest part must contain at least one type and that is easily done by just adding R1. That would mean that the rest of your code could be left unchanged:

template<typename Fn, typename H, typename R1, typename... R>
inline
void feh(Fn& fn, const std::tuple<H, R1, R...>*)
{
    fn(H{});
    using Rest = const std::tuple<R1, R...>*;
    feh<Fn, R1, R...>(fn, static_cast<Rest>(nullptr));
}

A C++17 solution would be to remove the other feh overloads and use a fold expression:

template <typename Fn, typename... H>
inline void feh(Fn& fn, const std::tuple<H...>*) {
    (..., fn(H{}));
}

This is a unary left fold over the comma operator which "unfolded" becomes:

(((fn(H1{}), fn(H2{})), ...), fn(Hn{}))
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
6

By far the simplest solution is an if constexpr :

template<typename  Fn, typename H, typename... R>
inline
void feh(Fn& fn, const std::tuple<H, R...>*)
{
    fn(H{});
    if constexpr (sizeof...(R) > 0) {
        using Rest = const std::tuple<R...>*;
        feh<Fn, R...>(fn, static_cast<Rest>(nullptr));
    }
}

and just remove the problematic overload.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • @TedLyngmo: My money is on GCC. `void feh(Fn& fn, const std::tuple*)` is more specialized, since every call to it would also match `void feh(Fn& fn, const std::tuple*)` but the reverse is not true (whenever `sizeof (R...)>0`) – MSalters Jun 17 '22 at 11:17
  • 1
    Yeah, could be. I suddenly got the feeling that it's more specialized too but I can't quite put my finger on it. If one removes the `tuple` wrapper, `clang++` doesn't have a problem anymore ([example](https://godbolt.org/z/nYjWz4MTx)) which makes me think that you are correct. – Ted Lyngmo Jun 17 '22 at 11:32
  • 3
    *"By far the simplest solution is an `if constexpr`"* I found fold expression even simpler: `(fn(R{}), ...);` – Jarod42 Jun 17 '22 at 11:35
0

How can I write this code so both compilers accept it?

You can write your code without recursion

template<typename Fn, typename ... Ts>
void fe(Fn& fn, const std::tuple<Ts...>* =  nullptr)
{
    // Trick to simulate fold expression of c++17
    const int dummy[] = {0, (static_cast<void>(fn(Ts{})), 0)...};
    static_cast<void>(dummy); // Avoid warning about unused variable
}

Which would become, in C++17

template<typename Fn, typename ... Ts>
void fe(Fn& fn, const std::tuple<Ts...>* =  nullptr)
{
    (static_cast<void>(fn(Ts{})), ...);
    // static_cast is here to handle evil overloaded operator comma (for type returned by Fn)
    // might be omitted if you know you are not in that pathological case
}

[Demo](fe(r, &tup);)

Jarod42
  • 203,559
  • 14
  • 181
  • 302