类在带默认参数的optional<unique_ptr>中前置声明编译失败问题
问题出在std::unique_ptr的默认删除器std::default_delete<T>上。当你在头文件里用= std::nullopt做默认参数时,编译器得在头文件里构造一个std::optional<std::unique_ptr<const Foo>>对象,这会触发std::unique_ptr<const Foo>的模板实例化——具体来说,std::default_delete<const Foo>会被实例化,而std::default_delete的operator()需要知道Foo的完整类型(因为要执行delete操作,删除不完整类型属于未定义行为,编译器会提前做检查)。但头文件里只有Foo的前向声明,没有完整定义,所以编译直接失败。
反观std::shared_ptr,它的删除器逻辑是延迟的:shared_ptr的控制块会存删除器,删除器的实例化和类型检查只会在真正要销毁对象的时候才发生(比如shared_ptr被析构或重置时)。用std::nullopt构造空的optional<shared_ptr<...>>时,根本不会触发删除器的实例化,所以哪怕Foo只有前向声明也能正常编译。
方案1:用空初始化器{}替代std::nullopt
直接把默认参数改成= {},这会调用std::optional的默认构造函数生成空对象,不会触发内部std::unique_ptr的构造和删除器实例化:
// DoSomething.h class Foo; void doSomething(std::optional<std::unique_ptr<const Foo>> f = {});
这个写法和原签名语义完全一致,都是默认传空optional,但避开了头文件里的std::default_delete实例化问题。
方案2:用辅助函数延迟默认参数构造
如果更倾向于显式写std::nullopt,可以把构造逻辑移到源文件里,用辅助函数中转:
// DoSomething.h class Foo; namespace detail { // 只声明,定义放到cpp文件里 std::optional<std::unique_ptr<const Foo>> get_default_f(); } // 默认参数调用辅助函数 void doSomething(std::optional<std::unique_ptr<const Foo>> f = detail::get_default_f());
// DoSomething.cpp #include "DoSomething.h" #include "Foo.h" // 这里有Foo的完整定义 namespace detail { std::optional<std::unique_ptr<const Foo>> get_default_f() { return std::nullopt; } } void doSomething(std::optional<std::unique_ptr<const Foo>> f) { // 函数实现 }
这种方式下,头文件里只需要辅助函数的声明,编译器不会在头文件里实例化函数体,自然不会触发std::default_delete的实例化。只有到源文件里处理构造逻辑时,Foo已经有完整定义了,就不会有问题。
内容的提问来源于stack exchange,提问作者Michael

