4

Consider this code:

#include <utility>

int foo_i(int x) { return x + 1; }
char foo_c(char x) { return x + 1; }

using II = int (*)(int);
using CC = char (*)(char);

template<typename F>
struct fn {
    F f;

    template<typename... Args>
    decltype(auto) operator()(Args&&... args) const
    {
        return f(std::forward<Args>(args)...);
    }
};

struct fn_2 : private fn<II>, private fn<CC> {
    fn_2(II fp1, CC fp2)
        : fn<II>{fp1}
        , fn<CC>{fp2}
    {}

    using fn<II>::operator();
    using fn<CC>::operator();
};

int main()
{
    fn_2 f(foo_i, foo_c);

    f(42);
}

Basically, fn<T> stores a functor (not necessarily a function pointer) of type T, and its variadic operator() forwards everything to the functor.

This code compiles fine with gcc 4.9.2 through gcc 6.1, but is rejected by every clang version I've tried, even clang 3.8. clang complains that the call is ambiguous. (I'd appreciate it if someone can try compile it with VS, because I don't have access to it right now.)

Which compiler is right, and how can I work around this discrepancy?

UPDATE: Although I'm still not sure which compiler's behavior is (more) compliant to the standard, I've found a workaround: Specializing fn<T> on pointers to functions, and avoid the need to blindly use variadic operator(). (Well, we've still left out pointers to member functions... For now I'm going to ignore them. :/) Example:

template<typename F>
struct fn : private F {
    using F::operator();
};

template<typename R, typename... Args>
struct fn<R (*)(Args...)> {
    fn(R (*f)(Args...)) noexcept : f_(f) {}

    R operator()(Args&&... args) const
    {
        return f_(std::forward<Args>(args)...);
    }

private:
    R (*f_)(Args...);
};
Zizheng Tai
  • 6,170
  • 28
  • 79
  • Well, there are two operator() functions that have different signatures. The two are overloads. The ambiguity comes from the possibility of implictly converting char to int. – Paul Stelian Jul 15 '16 at 08:51
  • @PaulStelian Then is it wrong for gcc to accept it? – Zizheng Tai Jul 15 '16 at 08:52
  • I don't think it's wrong *per se*. But I don't think it's strictly standard compliant either (try the -ansi parameter, this should disable gcc extensions) – Paul Stelian Jul 15 '16 at 08:55
  • @PaulStelian I tried `-ansi` but it [messed up everything](https://godbolt.org/g/r7H193). – Zizheng Tai Jul 15 '16 at 08:56
  • What commandline options did you use to compile ot? – MikeMB Jul 15 '16 at 08:58
  • @MikeMB `-std=c++14 -Wall -Wextra -pedantic` (it's included in the pages I link to in the question). – Zizheng Tai Jul 15 '16 at 08:59
  • A off-topic comment: this is c++14 code instead of c++11. – Mine Jul 15 '16 at 09:02
  • 2
    @PaulStelian FYI, GCC manual says that `-ansi` in C mode is equivalent to `-std=c90` and in C++ mode is equivalent to `-std=c++98`. Simple `-std=c++14` is enough. – HolyBlackCat Jul 15 '16 at 09:02
  • 1
    Note that with trailing return type syntax, both show an ambiguous call [Demo](http://coliru.stacked-crooked.com/a/7cf87367c7a343df) – Jarod42 Jul 15 '16 at 09:07
  • @Jarod42 Interesting, could there be something to do with SFINAE...? – Zizheng Tai Jul 15 '16 at 09:17
  • @HolyBlackCat Oh... Well, that's interesting. Thanks for telling me. – Paul Stelian Jul 15 '16 at 09:33
  • Since you asked, MSVC 2015 Update 3: `error C2668: 'fn::operator ()': ambiguous call to overloaded function`. Not a great error message (and neither is Clang's), but still an error, which is the correct behaviour. – bogdan Jul 15 '16 at 11:31
  • The only compiler giving a nice error message is EDG, which is what VS IntelliSense uses: `error: more than one instance of overloaded function "fn_2::operator()" matches the argument list: function template "decltype(auto) fn::operator()(Args &&...args) const [with F=II]" function template "decltype(auto) fn::operator()(Args &&...args) const [with F=CC]" argument types are: (int) object type is: fn_2` – bogdan Jul 15 '16 at 11:31
  • @bogdan Thanks for confirming! Could you try the example in my update and see if that works? – Zizheng Tai Jul 16 '16 at 02:10
  • Yes, that will solve the overload resolution problem, but it will cause an additional move for parameters taken by value; you'll need a little bit more type trait trickery to avoid that. Note that, for your initial example, it's not an issue of "which compiler's behavior is (more) compliant"; GCC is clearly wrong. There are two template overloads that generate specialization declarations that have the exact same parameters; there's no way to choose one over the other during overload resolution. GCC seems to always choose the first overload - `f('a');` will still end up calling `foo_i`. – bogdan Jul 16 '16 at 10:56
  • @bogdan Yes, this is only a toy example and I don't want to obscure the code with irrelevant logic. Thanks for confirming! – Zizheng Tai Jul 16 '16 at 11:08
  • @bogdan Actually, changing `R operator()(Args... args) const` to `R operator()(Args&&... args) const` should do the trick. – Zizheng Tai Jul 16 '16 at 11:21
  • No, that won't work. `Args&&` is not a forwarding reference there, as `Args` is not deduced from the call; those will always be rvalue references for non-reference `Args`. – bogdan Jul 16 '16 at 12:31
  • @bogdan *those will always be revalue references for non-reference `Args`* Ah, I was thinking for pass-by-value parameters move constructed from temporaries, this could work; I ignored the case when the parameters are copy constructed from lvalues. – Zizheng Tai Jul 16 '16 at 13:08

3 Answers3

2

I think clang is right here to not compile the code as the operator() is clearly ambiguous. If you think about it, from the provided template signature for operator() it's not clear which function should be preferred. You will have to provide additional hints to the compiler based on your stored function in fn.

Here is my solution:

#include <utility>
#include <type_traits>
#include <iostream>

int foo(int x) { return x + 1; }
char foo(char x) { return x + 1; }

using II = int (*)(int);
using CC = char (*)(char);

template <bool... B>
struct bool_pack {};

template <bool... V>
using all_true = std::is_same<bool_pack<true, V...>, bool_pack<V..., true>>;

template <typename... Args> struct packed {};

template <typename T> struct func_traits;

template <typename R, typename... Args>
struct func_traits<R(*)(Args...)> {
        using type = packed<Args...>;
};

template<typename F>
struct fn {
  F f;

  template<typename... Args,
           typename std::enable_if<std::is_same<packed<Args...>, typename func_traits<F>::type>::value>::type* = nullptr>
  auto operator()(Args&&... args) const
  {
    return f(std::forward<Args>(args)...);
  }
};

struct fn_2 : private fn<II>, private fn<CC> {
  fn_2(II fp1, CC fp2)
    : fn<II>{fp1}
    , fn<CC>{fp2}
  {}

  using fn<II>::operator();
  using fn<CC>::operator();
};

int main()
{
  fn_2 f(static_cast<II>(foo),
         static_cast<CC>(foo));

  std::cout << f(42) << std::endl;
  std::cout << f('a') << std::endl;
}

Nothing fancy, but I am using enable_if to help compiler choose the correct version of operator() based upon the arity types of the stored function.

Arunmu
  • 6,837
  • 1
  • 24
  • 46
  • But you have to give **exact** same type of argument (so lvalue cannot be used for rvalue). – Jarod42 Jul 15 '16 at 09:13
  • Note also that you wrongly use `std::enable_if`, it should not be as **default** template argument. Use `std::enable_if_t<...>* = nullptr` instead. – Jarod42 Jul 15 '16 at 09:15
  • There are two problems left: (1) You specialized for `func_traits`, but in my example the `T` in `fn` is not necessarily a function pointer. It could be any functor type. (2) Exactly as @Jarod42 said. Not only references: I can't pass a say a `long` to a `fn_2` anymore. – Zizheng Tai Jul 15 '16 at 09:15
  • @ZizhengTai Yes, I did know that would be the requirement but I just wanted to show an example of how it should be made to work. You will ofcourse have to make use of more generic version `func_traits` and make use of more type traits like `std::is_convertible` and comparing types only after `remove_const` and `remove_reference`. All that I guess is just more bolier plate code – Arunmu Jul 15 '16 at 09:18
  • Its all about making the SFINAE as generic as you want and no further – Arunmu Jul 15 '16 at 09:20
  • @Jarod42 For the `type` comment, see my comment above. I basically wanted to avoid writing lots of the boiler plate stuff :). And regarding use of `enable_if`, Its an unnamed template parameter, how would a use like that would be incorrect ? – Arunmu Jul 15 '16 at 09:22
  • If you use `is_convertible`, you will again have ambiguous call for the `char`/`int` conversion (as for the [trailing return type version](http://coliru.stacked-crooked.com/a/7cf87367c7a343df)). – Jarod42 Jul 15 '16 at 09:24
  • See [my answer](http://stackoverflow.com/questions/38305222/default-template-argument-when-using-stdenable-if-as-templ-param-why-ok-wit/38305320#38305320) for the `enable_if` part. – Jarod42 Jul 15 '16 at 09:26
  • There is `operator()(int)` and `operator()(char)`, which are different. My answer says why they can be ambiguous (char and int can implicitly convert between each other) – Paul Stelian Jul 15 '16 at 09:38
  • @Jarod42 Thanks, good to know. Regarding conversions, it's really up to the OP to allow and disallow the conversions. As the question is right now, defining the `operator()` as the way OP has done is ambiguous. It needs some kind of constraint through SFINAE. Whether long to int coversion is required but char to int shouldn't be done is should actually be taken care as required. – Arunmu Jul 15 '16 at 09:40
  • 1
    @PaulStelian What if OP decides to add one more function taking `long` later ? – Arunmu Jul 15 '16 at 09:44
  • @ZizhengTai Why don't you make `foo` taking `int` as a template function then with probably `is_numeric` constraint ? – Arunmu Jul 15 '16 at 09:46
  • @Arunmu My bad, two `foo`s are just bad naming. It is entirely irrelevant to the topic, I just copied them from my actual code. They can (and should) have different names. – Zizheng Tai Jul 15 '16 at 09:48
  • @Arunmu Perhaps that function shall also handle int. – Paul Stelian Jul 15 '16 at 15:00
2

This is a GCC bug. Note that GCC always calls the fn<II> version, even if called with a parameter of type char. There is no way a compiler can tell which function template to call, because they have the exact same signature, and GCC is just picking one arbitrarily.

Richard Smith
  • 13,696
  • 56
  • 78
  • Just a note to say that Clang's error message looks strange in this case - it seems to abruptly stop in the middle of a `note:`. Something similar happens to MSVC, funnily enough. Only EDG gave a nice and complete error message; it's quoted in the comments to the question. – bogdan Jul 23 '16 at 00:00
  • @bogdan This is working as intended: we suppress the code snippet on the second note because its location is exactly the same as the prior note, and there's no new information to show in it. – Richard Smith Jul 24 '16 at 03:10
0

The code would work perfectly fine if char and int were independent types with no implicit conversions. However, since char and int can be implicitly converted between each other (yes, int to char can convert implicitly!), there may be an ambiguity in the call. GCC does the intuitive exception in selecting the call requiring no conversion at all, if present.

EDIT: I've been pointed out there is a templated argument that gets its own operator() function in here. This is something I definitely did not see.

Paul Stelian
  • 1,381
  • 9
  • 27
  • 2
    No, it wouldn't be fine, because the overload resolution takes place, before the arguments are forwarded to the actual functions and both base classes have a templated `operator()` that just takes ANY argument. (just check with e.g. `std::string`). – MikeMB Jul 15 '16 at 10:23
  • Oh did not spot that. – Paul Stelian Jul 15 '16 at 15:00