July 14th, 2020

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

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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

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

  • James Touton

    * 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

      Came here to report these as well.

  • Billy O'Neal

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

    • Kalle Niemitalo

      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.