Reputation: 6010
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.
Upvotes: 23
Views: 1838
Reputation: 1
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 thanG
ifF
is at least as specialized asG
andG
is not at least as specialized asF
.
(emphasis mine)
Upvotes: 17
Reputation: 217135
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);)
Upvotes: 0
Reputation: 117268
I'm unsure which compiler that is correct, but...clang++
is correct because both functions matches equally good.
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{}))
Upvotes: 11
Reputation: 179779
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.
Upvotes: 6