3

I'm encountering the following issue with GCC (tested with v6.4 it's the same behavior on the current trunk) which can be reduced to the following minimalistic example:

Further we look at callable objects such as classes that implement operator() and operator bool and function pointers such as void(*)() and function references void(&)().

Following questions might be relevant to be read in advance:

I'm trying to implement a conditional invoke which checks before a callable is invoked whether its conversion to bool is true before invoking it:

/// g++-6.4 -O3 -std=c++17 -Wall

#include <functional>
#include <type_traits>

template <typename O>
void safe_invoke(O&& fn) {
  if (fn) {
    std::invoke(std::forward<O>(fn));
  }
}

void somefn() { }

int main() {
    safe_invoke(somefn);
    return 0;
}

Will yield a warning when using GCC and -Wall

In instantiation of 'void safe_invoke(O&&) [with O = void (&)()]':
  warning: the compiler can assume that the address of 'fn' will always evaluate to 'true' [-Waddress]
    if (fn) {
    ^~

As indicated by the warning GCC uses void(&)() as the correct reference type of the callable type O. My approach in dealing with this warning was that I want to get completely rid of the bool(callable) check for a function references which can never be null by specializing those with a specific trait:

/// g++-6.4 -O3 -std=c++17 -Wall -Werror
/// https://gcc.godbolt.org/z/2TCaHq

#include <functional>
#include <type_traits>

template <typename T>
struct invoke_trait {
  template <typename O>
  static void invoke(O&& fn) {
    if (fn) {
      std::invoke(std::forward<T>(fn));
    }
  }
};

template <typename Ret, typename... Args>
struct invoke_trait<Ret (*)(Args...)> {
  template <typename O>
  static void invoke(O&& fn) {
    if (fn) {
      std::invoke(std::forward<O>(fn));
    }
  }
};

template <typename Ret, typename... Args>
struct invoke_trait<Ret (&)(Args...)> {
  template <typename O>
  static void invoke(O&& fn) {
    std::invoke(std::forward<O>(fn));
  }
};

template <typename O>
void safe_invoke(O&& fn) {
  using trait_t = invoke_trait<std::decay_t<O>>;
  trait_t::invoke(std::forward<O>(fn));
}

void test() {
}

int main() {
  // No compile error as expected:
  {
    using fn_t = void (*)();
    fn_t fn = nullptr;

    safe_invoke(fn);
  }

  // the compiler can assume that the address of 'fn' will always evaluate
  // to 'true' [-Werror=address]
  {
    safe_invoke(test);
  }

  // the compiler can assume that the address of 'fn' will always evaluate
  // to 'true' [-Werror=address]
  {
    using fn_ref_t = void (&)();
    fn_ref_t fn_ref = test;

    safe_invoke(fn_ref);
  }

  return 0;
}

https://gcc.godbolt.org/z/3QAKpf

Sadly GCC fails here and always uses the specialization for Ret (*)(Args...). Is there an issue with my code which prevents the correct specialization to Ret (&)(Args...) or can this specialization be done differently? Additionally is there a different way to prevent the GCC warning without suppressing it explicitly (although this might not be the optimal solution)?

Naios
  • 1,513
  • 1
  • 12
  • 26

2 Answers2

3
std::decay_t<O>

This converts function references to function pointers.

Replace decay with a combination of remove reference and remove CV. Then specialize on F(Args...) and F(*)(Args...) instead of F(&)(Args...) and F(*)(Args...).

template <typename Ret, typename... Args>
struct invoke_trait<Ret (*)(Args...)> {
  template <typename O>
  static void invoke(O&& fn) {
    if (fn) {
      std::invoke(std::forward<O>(fn));
    }
  }
};

template <typename Ret, typename... Args>
struct invoke_trait<Ret(Args...)> {
  template <typename O>
  static void invoke(O&& fn) {
    std::invoke(std::forward<O>(fn));
  }
};

template <typename O>
void safe_invoke(O&& fn) {
  using trait_t = invoke_trait<std::remove_cv_t<std::remove_reference_t<O>>>;
  trait_t::invoke(std::forward<O>(fn));
}

That should work, up to typos.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • `using trait_t = invoke_trait>>;` doesn't fix the issue here, but good catch of you that `decay` converts the function reference to a pointer. Probably the issue is also that I've used `Ret (&)(Args...)` instead of `Ret (Args...)`, – Naios Sep 17 '19 at 20:58
  • @Timo What, are you saying that `int(&)(int, int)` passed to `std::remove_reference_t` returns `int(*)(int, int)`? Check again. – Yakk - Adam Nevraumont Sep 17 '19 at 21:22
  • @DenisBlank No, remove reference first, then remove cv. – Yakk - Adam Nevraumont Sep 17 '19 at 21:23
  • Good catch. I corrected that and also marked your answer as the solution because you additionally spotted the issue with the `Ret(Args...)`. Thanks for your help. – Naios Sep 17 '19 at 21:37
  • @Yakk-AdamNevraumont my bad, I didn't notice that the compiler error changed. The generalized trait threw the warning instead of the pointer trait. – Timo Sep 17 '19 at 22:09
3

Your problem lies in the safe_invoke function:

template <typename O>
void safe_invoke(O&& fn) {
  using trait_t = invoke_trait<std::decay_t<O>>;
  trait_t::invoke(std::forward<O>(fn));
}

At this point O might be a function reference, but std::decay_t<O> will decay it to a function pointer. To quote cppreference on this:

Otherwise, if T is a function type F or a reference thereto, the member typedef type is std::add_pointer::type.

The decayed type will therefore always choose the pointer trait.

Maybe use

using trait_t = invoke_trait<std::remove_cv_t<O>>;

instead.

Timo
  • 9,269
  • 2
  • 28
  • 58