在具有移动语义的RAII类中,默认构造函数应该做什么

What should the default constructor do in a RAII class with move semantics?

本文关键字:构造函数 默认 什么 类中 移动 语义 RAII      更新时间:2023-10-16

Move语义对RAII类非常有用。它们允许人们像有值语义一样编程,而不需要大量拷贝。一个很好的例子是从函数返回std::vector。然而,使用值语义进行编程意味着,人们期望类型的行为类似于原始数据类型。这两个方面有时似乎不一致。

一方面,在RAII中,人们会期望默认构造函数返回一个完全初始化的对象,或者在资源获取失败时抛出异常。这保证了任何构建的对象都将处于有效和一致的状态(即安全使用)。

另一方面,在移动语义中,存在对象处于有效但未指定状态的点。类似地,基元数据类型可能处于未初始化状态。因此,对于值语义,我希望默认构造函数在这种有效但未指定的状态下创建一个对象,以便以下代码具有预期的行为:

// Primitive Data Type, Value Semantics
int i;
i = 5;
// RAII Class, Move Semantics
Resource r;
r = Resource{/*...*/}

在这两种情况下,我都希望"重"初始化只发生一次。我想知道,这方面的最佳实践是什么?显然,第二种方法存在一个小的实际问题:如果默认构造函数创建处于未指定状态的对象,那么如何编写一个获取资源但不使用额外参数的构造函数?(想到标签调度…)

编辑:一些答案对试图使类像原始数据类型一样工作的原理提出了质疑。我的一些动机来自Alexander Stepanov的《组件高效编程》,他在书中谈到了正则类型。我要特别指出:

无论c中的自然惯用表达式是什么(对于内置类型),都应该是正则类型的自然惯用表达。

他接着提供了与上述几乎相同的例子。在这种情况下,他的观点不成立吗?我理解错了吗?

编辑:由于没有太多讨论,我即将接受最高投票的答案。在默认构造函数中初始化处于"类似移动"状态的对象可能不是一个好主意,因为每个同意现有答案的人都不会期望这种行为。

然而,使用值语义编程意味着类型的行为类似于基元数据类型。

关键词"喜欢"。不是"等同于"。

因此,对于值语义,我希望构造函数来创建处于此有效但未指定状态的对象

我真的不明白你为什么会这样。对我来说,这似乎不是一个很理想的功能。

这方面的最佳实践是什么?

忘记非POD类应该与基元数据类型共享此特性的想法吧。这是错误的头脑。如果没有合理的方法来初始化一个没有参数的类,那么该类就不应该有默认的构造函数。

如果您想声明一个对象,但推迟初始化它(可能在更深的范围内),那么使用std::unique_ptr

如果您接受对象通常通过构造是有效的,并且对对象的所有可能操作都应该只在有效状态之间移动它,那么在我看来,通过使用默认构造函数,您只需要说两件事中的一件:

  • 这个值是一个容器,或者是另一个具有合理"空"状态的对象,我打算对其进行变异——例如std::vector

  • 该值没有任何成员变量,主要用于其类型,例如std::less

这并不意味着从对象中移出的对象必须具有与默认构造对象相同的状态。例如,包含空字符串""std::string可能与从string移动的实例具有不同的状态。当您默认构造一个对象时,您希望使用它;当你离开一个物体时,绝大多数时候你只是简单地摧毁它。

如何编写一个获取资源但不需要额外参数的构造函数?

如果你的默认构造函数很昂贵并且不带参数,我会质疑为什么。它真的应该做这么贵的事情吗?它的默认参数来自哪里——一些全局配置?也许明确地传递它们会更容易维护。以std::ifstream为例:通过一个参数,它的构造函数打开一个文件;如果没有,则使用open()成员函数。

您可以做的是延迟初始化:在对象中设置一个标志(或空指针),指示对象是否已完全初始化。然后有一个成员函数,它使用此标志来确保在运行后初始化。默认构造函数所需要做的就是将初始化标志设置为false。如果所有需要初始化状态的成员在开始工作之前都调用ensure_initialization(),那么您就拥有了完美的语义,并且没有双重繁重的初始化。

示例:

class Foo {
public:
    Foo() : isInitialized(false) { };
    void ensureInitialization() {
        if(isInitialized) return;
        //the usual default constructor code
        isInitialized = true;
    };
    void bar() {
        ensureInitialization();
        //the rest of the bar() implementation
    };
private:
    bool isInitialized;
    //some heavy variables
}

编辑:为了减少函数调用产生的开销,您可以这样做:

//In the .h file:
class Foo {
public:
    Foo() : isInitialized(false) { };
    void bar();
private:
    void initialize();
    bool isInitialized;
    //some heavy variables
}
//In the .cpp file:
#define ENSURE_INITIALIZATION() do { 
    if(!isInitialized) initialize(); 
} while(0)
void Foo::bar() {
    ENSURE_INITIALIZATION();
    //the rest of the bar() implementation
}
void Foo::initialize() {
    //the usual default constructor code
    isInitialized = true;
}

这样可以确保在不内联初始化本身的情况下内联是否初始化的决定。后者只会使可执行文件膨胀并降低指令缓存效率,但前者不能自动完成,因此需要使用预处理器。这种方法的开销平均应该小于一个函数调用的开销。