Search code examples
c++language-lawyerc++20

Why is void() legal whereas void{} is not?


While working on a template this evening I came across something surprising. The template in question made use of std::invoke to call a function when a certain set of conditions are met otherwise the default constructed return type is returned after logging an error message. This was working well until I needed to apply the template to a function returning void. After speaking with someone more knowledgeable than myself, he pointed out that using parenthesis rather than braces for constructing the return type would be compatible with void.

Sure enough, this works with any compiler I've been able to throw at it but it bothers me on a deeper level. In the code below R() gives the appearance of being default constructed, but I suspect this is really a deviant case of a function-style conversion (eg. void()), which explains why using R{} (eg. void{}) is considered illegal by most (but not all) compilers.

Are there any language lawyers around who can verify the above and or provide advice on the most idiomatic way to implement the following template without creating two versions using concepts? (I'm currently targeting C++20).

template<typename F, typename... Args,
         typename R = std::invoke_result_t<F, Args...>>
auto call(F&& f, Args&&... args)
{
    if (some_runtime_condition) [[unlikely]] {
        // log an error message
        return R(); // R{} is invalid for functions returning void
    }

    return std::invoke(std::forward<F>(f), std::forward<Args>(args...));
}

https://godbolt.org/z/fos4T6Wfj

TIA!


Solution

  • So there are two things going on here at the same time. First, prior to C++20 there was a defect in the standard where only the expression void() was allowed, this was a carry over from C++03 and was never updated in C++11 through C++17 to also allow void{} to do the same thing.

    Since C++20 the wording of the standard is [expr.type.conv]/2:

    If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression. Otherwise, if the type is cv void and the initializer is () or {} (after pack expansion, if any), the expression is a prvalue of type void that performs no initialization. Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized with the initializer. If the initializer is a parenthesized optional expression-list, the specified type shall not be an array type.

    emphasis mine

    And that means your code should compile, and indeed it does compile with GCC and MSVC, just not clang