0

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.

Michael
  • 172
  • 12
  • 2
    Perhaps don't use optional pointers. Pointers already have an out-of band value `nullptr`, so consider using it instead. – n. m. could be an AI Jul 02 '23 at 12:21
  • *"removing the `=std::nullopt` default argument, everything compiles fine."*, So you have a solution, overload function instead of default argument. – Jarod42 Jul 02 '23 at 12:37
  • You may want the `language-lawyer` tag, if you want chapter-and-verse from the standard why unique_ptr triggers this behavior. Otherwise, I'd omit the default parameter value, and define an overload `void doSomething();` where the implementation just calls the other one with `std::nullopt` argument. – Eljay Jul 02 '23 at 12:53
  • @n.m.willseey'allonReddit My code example is different, and I created a minimal example that the error re-occurs. I know it is possible to write different code that works, what I don't understand is why this code doesn't, even though I never use the definition of `Foo`. – Michael Jul 02 '23 at 12:59
  • Perhaps it is possible to forward-declare that `Foo` destructor or something similar that I'm not aware of to avoid the error, this is why I'm asking the question, I don't want to modify the signature of `doSomething` – Michael Jul 02 '23 at 13:00

1 Answers1

2

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.

chrysante
  • 2,328
  • 4
  • 24
  • Thanks for the answer. I don't want to change the signature of the function. This exact code compiles fine with shared_ptr, is shared_ptr destructible for incomplete `T`s ? – Michael Jul 02 '23 at 13:05
  • 1
    Yes, `shared_ptr` is destructible for incomplete `T`. Note that `shared_ptr` unlike `unique_ptr` does not have a `Deleter` template parameter. It uses a type erased deleter that is stored during construction and assignment of the `shared_ptr` object. – chrysante Jul 02 '23 at 13:13
  • But it is possible to forward declare `T`s for unique_ptr when the unique_ptr is stored as a member of a class, or even in this example when the `= std::nullopt` is removed. Is this a standard limitation? or should I define something else? Becuase I don't see any reason why the standard would limit me from doing this – Michael Jul 02 '23 at 13:25
  • It is only possible to store a `unique_ptr` of forward declared `T` as a class member if the destructor (and the move operations) of that class are defined out-of-line after the definition of `T`. And when you remove `= std::nullopt`, no object of type `std::optional>` will be instantiated at the declaration of the function. In that case you are only naming the type, which does not have any further requirements. – chrysante Jul 02 '23 at 13:30