c++c++17language-lawyer

is optional<void> type name valid?


I discovered by accident while modifying existing code that an alias declaration created std::optional<void>. It came from template code like this:

using ret_t = std::invoke_result_t<Fn, Args...>;
using opt_ret_t = std::optional<ret_t>;

The code later discriminates the case when the return type is (not) void, so there is never actually an optional object created.

To make sure I wasn't missing something, I added an assert in that branch of code and everything compiles:

static_assert(std::is_same_v<opt_ret_t, std::optional<void>>);

cppreference says:

There are no optional references, functions, arrays, or cv void; a program is ill-formed if it instantiates an optional with such a type.

So regardless of compilers not complaining, is this code ill-formed?


Solution

  • [res.on.functions]/2.5 states that the behaviour is undefined if you use an incomplete type as a template argument "when instantiating a template component or evaluating a concept, unless specifically allowed for that component" in the standard library.

    void is an incomplete type ([basic.types.general]/5), and std::optional is not one of the standard library templates that allows incomplete types ([optional.optional.general]/3). So, if you instantiate std::optional<void>, the program may fail to compile or it may compile and behave unpredictably at runtime (but in practice, the first thing is what happens).

    However, doing something like this

    using T = std::optional<void>;
    

    does not instantiate std::optional<void>. According to [temp.inst]/2 and [temp.inst]/11, a class template specialization is implicitly instantiated only when it is used in a way that requires the specialization to be complete, or when the completeness of the specialization affects the semantics of the program. When you merely mention the name of a class template specialization or declare an alias for it, the class template specialization doesn't need to be complete, nor is there any difference between declaring an alias to a complete or incomplete type. So in this case we do not get an implicit instantiation.

    The standard library does not have a corresponding rule for non-instantiating uses of class template specializations. Indeed, if U is an incomplete class type then it is useful to be able to do something like this:

    class U;
    
    std::optional<U> getU();
    
    class U { /* ... */ };
    // U is now complete
    
    std::optional<U> getU() {
        // define the function
    }
    

    Using a type as a return type doesn't require it to be complete until the function is actually defined, so it's legal to use std::optional<U> as a return type on a non-defining declaration prior to U being completed.

    If there were any rule specifically disallowing non-instantiating uses of std::optional<void> in particular, it would need to be found in the description of std::optional itself. Since std::optional is defined as

    namespace std {
      template <class T>
      class optional {
          // ...
      };
    
      // (deduction guide omitted)
    }
    

    nothing bad will happen when you merely pass void to the template parameter T without instantiating the class template, but as Davis Herring points out, there could be some class templates where merely mentioning the name of a specialization would be invalid even without instantiation. std::optional just isn't one of them.