Deconstructing function pointers in a C++ template, the noexcept complication

Raymond Chen

Last time, we put together a little traits class to decompose a function pointer into its components. But one thing missing from our class is the noexcept qualifier.

For the remainder of the discussion, I’ve removed the FirstĀ­Arg and LastĀ­Arg type aliases, since I came to the conclusion that they aren’t really needed. What’s left is this:

template<typename R, typename... Args>
struct FunctionTraitsBase
{
  using RetType = R;
  using ArgTypes = std::tuple<Args...>;
  static constexpr std::size_t ArgCount = sizeof...(Args);
  template<std::size_t N>
  using NthArg = std::tuple_element_t<N, ArgTypes>;
};

template<typename F> struct FunctionTraits;

template<typename R, typename... Args>
struct FunctionTraits<R(*)(Args...)>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(*)(Args...);
};

But it falls apart when we give it a noexcept function pointer. (Note that noexcept did not become part of the function pointer type until C++17.)

void f()
{
  using T = int(*)() noexcept;
  using R = FunctionTraits<T>::RetType; // error
}

There is no match for T because none of our specializations support noexcept function pointers.

So let’s add noexcept to our signatures. Let’s try this version, which takes advantage of the fact that noexcept takes a Boolean parameter that says whether the noexcept applies. Saying noexcept with no parameters is shorthand for noexcept(true), and omitting noexcept is the same as noexcept(false).

template<typename R, typename... Args, bool Nonthrowing>
struct FunctionTraits<R(*)(Args...) noexcept(Nonthrowing)>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(*)(Args...) noexcept(Nonthrowing);
  static constexpr bool IsNoexcept = Nonthrowing;
};

The Microsoft compiler doesn’t like it:

// MSVC
error C2057: expecting constant expression
    struct FunctionTraits<R(*)(Args...) noexcept(Nonthrowing)>
                                                 ^^^^^^^^^^^
error C27027: 'Nonthrowing': template parameter not used or deducible

icc also doesn’t like it, but for a different reason: It’s perfectly happy to match the partial specialization to a non-noexcept function, but thinks it doesn’t apply to a noexcept function.

    // icc is okay with this
    using Test1 = FunctionTraits<int(*)(float) noexcept>;

    // but not this. "error: incomplete type is not allowed"
    using Test2 = FunctionTraits<int(*)(float) noexcept>;

On the other hand, gcc and clang are okay with it and deduce Nonthrowing appropriately. I’m not sure who is right. (I didn’t check icc.)

Well that’s a bummer. The parameter to noexcept is not deducible by the Microsoft compiler. We’ll just have to add a separate specialization.

template<typename R, typename... Args>
struct FunctionTraits<R(*)(Args...)>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(*)(Args...);
  constexpr static bool IsNoexcept = false;
};

template<typename R, typename... Args>
struct FunctionTraits<R(*)(Args...) noexcept>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(*)(Args...);
  constexpr static bool IsNoexcept = true;
};

Okay, so that takes care of the noexcept wrinkle. We’ll look at another attribute next time.

Update: Paragraph [temp.deduct.type]/8 of the C++ specification lists the deducible contexts, and the noexcept specifier is not on the list. Therefore, MSVC is correct to reject it, and gcc and clang’s behavior are nonstandard extensions. This was tracked as Core Working Group issue number CWG2355, with a vote to revise the standard passing in January 2022 and accepted on May 21, 2022. MSVC implemented the language change in February 2020.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Billy O'Neal 0

    Don’t forget the ‘abominable’ complication. (And the Windows-specific calling convention complication)

    • Kalle Niemitalo 0

      These templates deconstruct function pointer types, not function types. Does C++ now allow abominable function pointer types?

      Ugh, the extern "C" vs. extern "C++" complication now that language linkage is part of the function type.

  • James Touton 0

    * Test1 and Test2 are identical; I think you meant to omit the noexcept in Test1.
    * You have a note that says you didn’t check icc, right after describing the behavior of icc.
    * IsNoexcept is useful, but Pointer should retain the noexcept specifier, matching the template argument exactly.

    • John McPhersonMicrosoft employee 0

      Came here to report these as well.

Feedback usabilla icon