C++11如何将头文件声明的成员对象构造延迟到源文件
C++11下延迟构造类成员的实现方案
你遇到的编译错误核心来自两个问题:一是头文件里写了非法的前置声明class Bar(string First, string Second);,C++前置声明只需要写class Bar;即可,不能带构造参数列表;二是对成员对象的构造时机有误解——类内声明成员对象不会立刻触发构造,成员的构造完全发生在构造函数的初始化列表阶段,只要你显式调用对应构造函数,就不会触发默认构造的调用。
场景1:允许在foo.h中引入Bar的完整定义
如果不需要在头文件隐藏Bar的实现,根本不需要任何特殊技巧,直接写即可,完全不会调用Bar的默认构造:
// foo.h #include "bar.h" // 直接引入Bar的头文件,删掉错误的带参前置声明 class Foo : public Base { public: Foo(int a); Bar b; // 仅声明成员,不触发构造 // 其余成员 };
// foo.cc Foo::Foo(int a) : Base(a) , b("first", "second") // 此处显式调用Bar的带参构造,无默认构造调用 {}
你之前遇到的brace-enclosed initializer list报错,就是因为错误的前置声明导致编译器不认识Bar类型,只要修正前置声明、正确包含Bar的头文件即可解决。
场景2:需要在头文件隐藏Bar的定义(仅前置声明Bar)
如果因为编译依赖等原因不能在foo.h包含bar.h,就需要用间接持有方案,你提到的裸指针改动大的问题可以通过以下两个方案规避:
- 方案1:std::unique_ptr
(工业界通用首选,改动极小)
不需要用裸指针,C++11的std::unique_ptr只需要你把原有代码里的b.xxx访问改成b->xxx,改动量极低,且自动管理内存,不需要手动释放:
// foo.h #include <memory> class Bar; // 正确前置声明即可 class Foo : public Base { public: Foo(int a); ~Foo(); // 必须显式声明析构,在源文件实现 std::unique_ptr<Bar> b; // 其余成员 };
// foo.cc #include "bar.h" Foo::Foo(int a) : Base(a) , b(new Bar("first", "second")) // 源文件中构造Bar {} Foo::~Foo() = default; // 源文件中实现析构,此时Bar为完整类型,无编译错误
- 方案2:aligned_storage 做值语义延迟构造(几乎不改动访问语法)
如果你完全不想把.访问改成->,可以用C++11标准库的std::aligned_storage预分配一块和Bar大小、对齐一致的原始内存,在源文件中用placement new构造Bar,对外保持值语义访问:
// foo.h #include <type_traits> class Bar; class Foo : public Base { public: Foo(int a); ~Foo(); // 不需要拷贝/移动可以直接delete,需要的话自行在源文件实现 Foo(const Foo&) = delete; Foo& operator=(const Foo&) = delete; Bar& b() { return *reinterpret_cast<Bar*>(&b_storage); } const Bar& b() const { return *reinterpret_cast<const Bar*>(&b_storage); } private: typename std::aligned_storage<sizeof(Bar), alignof(Bar)>::type b_storage; // 其余成员 };
// foo.cc #include "bar.h" Foo::Foo(int a) : Base(a) { // 在预分配内存上调用Bar的带参构造 new (&b_storage) Bar("first", "second"); } Foo::~Foo() { // 显式调用Bar的析构函数 reinterpret_cast<Bar*>(&b_storage)->~Bar(); }
使用时只需要把原有b.xxx改成b().xxx,改动量非常小。
关于std::reference_wrapper的适用性
std::reference_wrapper完全不适合这个场景:
- 它本质是引用的可复制包装,不持有对象内存,只是绑定到一个已经存在的Bar实例上,本身不解决对象构造的问题
- 如果尝试把构造函数里创建的临时Bar对象绑定给它,临时对象会在初始化列表结束后立刻析构,直接产生悬空引用,触发未定义行为
- 引用语义的成员一旦初始化就无法重新绑定,灵活性远低于上述两个方案。
内容的提问来源于stack exchange,提问作者Lance E.T. Compte
相关产品推荐
相关产品推荐

