9

移动语义非常适合 RAII 类。它们允许人们像具有价值语义一样进行编程,而无需大量复制。一个很好的例子是从函数返回 std::vector。然而,使用值语义进行编程意味着,人们会期望类型表现得像原始数据类型。这两个方面有时似乎不一致。

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

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

// Primitive Data Type, Value Semantics
int i;
i = 5;

// RAII Class, Move Semantics
Resource r;
r = Resource{/*...*/}

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

编辑:一些答案质疑试图让你的类像原始数据类型一样工作的基本原理。我的一些动机来自Alexander Stepanov 的 Efficient Programming with Components,其中他谈到了常规类型。特别是,让我引用:

无论是 c [用于内置类型] 中的自然惯用表达式,都应该是正则类型的自然惯用表达式。

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

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

4

3 回答 3

7

然而,使用值语义进行编程意味着,人们会期望类型表现得像原始数据类型。

关键字“喜欢”。不是“完全相同”。

因此,使用值语义,我希望默认构造函数以这种有效但未指定的状态创建对象

我真的不明白为什么你应该期待。对我来说,这似乎不是一个非常理想的功能。

这方面的最佳做法是什么?

忘记非 POD 类应该与原始数据类型共享此功能的想法。方向不对。如果没有合理的方法来初始化没有参数的类,那么该类不应该有默认构造函数。

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

于 2013-08-11T05:15:39.840 回答
5

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

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

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

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

如何编写一个获取资源但不带附加参数的构造函数?

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

于 2013-08-11T05:28:20.487 回答
1

您可以做的是延迟初始化:在您的对象中有一个标志(或空指针),指示该对象是否已完全初始化。然后有一个成员函数使用这个标志来确保它运行后的初始化。您的默认构造函数需要做的就是将初始化标志设置为 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;
}

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

于 2013-08-17T07:15:06.410 回答