Search code examples
c++stdoptional

Forward declaration of class inside unique_ptr inside optional with default argument fails


I have the following piece of code

// DoSomething.h
class Foo;
void doSomething(std::optional<std::unique_ptr<const Foo>> f = std::nullopt);

If I include DoSomething.h anywhere without the definition of Foo the file doesn't compile, even if it doesn't even call the doSomething function. In contrast, removing the =std::nullopt default argument, everything compiles fine.

My assumption is the issue has something to do with the deleter interface of the unique_ptr, and my question is how to solve this issue so I can forward declare Foo in this scenario?

Thanks

Edit: I don't want to change the signature of this function, I know I can solve this with overloading or removing the optional use. I want to understand why this doesn't work.

Following note - this same code compiles perfectly fine when switching the std::unique_ptr with std::shared_ptr. It means it should be possible to make it work with std::unique_ptr. I know their deleteres have slightly different interface, and I'm not familiar enough with it.


Solution

  • Cppreference says the following about the template argument of std::optional:

    T - the type of the value to manage initialization state for. The type must meet the requirements of Destructible

    • std::unique_ptr<T> is not destructible when T is incomplete.

    • The default arguments of a function are instantiated at the declaration of that function.

    Therefore the requirements of std::optional are not satisfied at the point where the object is instantiated.

    As noted in the comments, you can solve the issue by overloading the function instead of using default arguments. Also pointers expose a null value, so in most cases std::optional<pointer-type> is not necessary.


    Follow up after reading your edit:

    class Foo;
    using FooDeleter = std::function<void(Foo const*)>;
    void doSomething(std::optional<std::unique_ptr<const Foo, FooDeleter>> f = std::nullopt);
    

    This code will compile fine. It uses std::unique_ptr with a type erased deleter similar to std::shared_ptr. The problem if you instantiate unique_ptr with the default deleter (std::default_delete) is this: A simplified definition of std::default_delete would look something like this:

    template <typename T>
    class default_delete {
        void operator()(T* p) const {
            p->~T();
        }
    };
    

    So during the instantiation of std::optional<std::unique_ptr<Foo>> the code above will be instantiated. Because it makes a call to the destructor of Foo that is not declared compilation will fail. If you instantiate std::optional<std::unique_ptr<Foo, FooDeleter>>, only operator() of std::function<...> will be called, which is independent of the definition of the destructor of Foo.