c++对象是否总是处于有效状态?

Should a C++ object always be in a valid state?

本文关键字:有效状态 对象 是否 c++      更新时间:2023-10-16

当一个对象被构造时,构造函数应该总是让它处于"初始化"状态吗?

例如,如果一个图像类有两个构造函数,其中一个接受文件路径字符串,而另一个不接受参数,后者使图像对象处于无效状态是不好的做法吗?

假设类被编写来处理这两种状态

我问这个问题是因为我发现在很多情况下几乎有必要有一个默认结构。特别是当一个对象是类的成员,并且你想在该类的构造函数中初始化它时。

编辑:我知道成员初始化列表。我发现了一些情况,我想在类的构造函数期间构造对象,而不是之前。虽然,我知道这可能比其他选择更危险。

这一切归结为"有效状态"的定义:如果你的类的方法处理路径为空时的状态,那么具有空路径的状态是有效状态,并且绝对是可接受的。

但是,从编码的角度来看,这可能不是最优的,因为您可能需要为路径的有效性添加多个检查。通常可以通过实现状态模式来管理复杂性。

我发现在很多情况下有一个默认结构几乎是必要的。特别是当一个对象是类的成员,并且你想在该类的构造函数中初始化它时。

只要在初始化列表中构造依赖对象,就不需要在其所属类的构造函数中初始化对象的默认构造函数。

你的最后一行:

我问这个问题是因为我发现在很多情况下几乎有必要有一个默认结构。特别是当一个对象是类的成员,并且你想在该类的构造函数中初始化它时。

表示没有使用成员初始化列表。在这种情况下,不需要默认构造函数。例子:

class Member {
public:
  Member(std::string str) { std::cout << str << std::endl; }
};
class Foo {
public:
  Foo() : member_("Foo") {}
private:
  Member member_;
}

此外,你的问题标题和正文冲突,术语有点模糊。在构造时,通常最好让对象处于有效和可用的状态。有时第二个方面(可用性)不是那么必要,许多解决方案都需要它。此外,在c++ 11中,从对象移动必须使其处于有效状态,但不一定(在许多情况下不应该)使其处于可用状态。

EDIT:要解决在构造函数中执行工作的问题,请考虑将工作移动到member类的静态成员或所属类中的私有(静态或非静态)函数中:

class Member {
public:
  Member(std::string str) { std::cout << str << std::endl; }
};
class Foo {
public:
  Foo() : member_(CreateFoo()) {}
private:
  Member CreateMember() {
    std::string str;
    std::cin >> str;
    return Member(str);
  }
  Member member_;
};
但是,这种方法的一个危险是,如果使用非静态成员函数进行创建,则初始化顺序可能很重要。静态函数要安全得多,但是您可能希望传递一些其他相关的成员信息。记住,初始化是按照类中成员声明的顺序进行的,而不是按照初始化列表声明的顺序。

是的,它应该总是有效的。然而,通常没有很好地定义是什么使对象有效。至少,对象应该以一种不会崩溃的方式可用。但是,这并不意味着可以对对象执行所有操作,但至少应该有一个操作。在许多情况下,这只是来自另一个源的assignment(例如std容器迭代器)和destruction(即使在移动之后,这也是强制性的)。但是,对象在任何一种状态下支持的操作越多,出错的可能性就越小。

这通常是一种权衡。如果您可以让对象只具有所有操作都有效的状态,那当然很棒。但是,这种情况很少见,如果您必须克服重重困难才能到达那里,那么通常只需在其某些功能中添加并记录先决条件就会更容易。在某些情况下,您甚至可以拆分接口,以区分进行这种权衡的函数和不进行这种权衡的函数。一个流行的例子是在std::vector中,您需要有足够的元素作为使用operator[]的先决条件。另一方面,at()函数仍然可以工作,但是会抛出一个异常。

首先,让我们定义什么是"有效状态":它是对象可以工作的状态。
例如,如果我们正在编写一个管理文件的类,并允许我们读写该文件,那么有效状态(遵循我们的定义)可能是对象持有正确打开的文件并准备对其进行读写的状态。

但是考虑其他情况:移动的右值的状态是什么?

File::File( File&& other )
{
    _file_handle = other._file_handle;
    other._file_handle = nullptr; //Whats this state?
} 

这是一种状态,文件对象还没有准备好对文件进行读写,但是已经准备好初始化。也就是说,是一个准备初始化状态的

现在考虑使用复制和交换习语的上述因子的另一种实现:

File::File() :
    _file_handle{ nullptr }
{} 
File::File( File&& other ) : File() //Set the object to a ready to initialice state
{
    using std::swap; //Enable ADL
    swap( *this , other );
}

在这里,我们使用默认的函数将对象置于准备初始化状态,并将传递的右值与该对象交换,结果与第一个实现完全相同。正如我们在上面看到的,一个是准备工作状态,对象准备做应该做的事情的状态,另一个完全不同的是准备初始化状态:对象还没有准备好工作,但准备初始化并设置为工作的状态。

我对你的问题的回答是:一个对象并不总是处于有效状态(它并不总是准备好被使用),但如果它不准备好被使用,它应该准备好被初始化,然后准备好工作。

正常情况下,是的。我看到过一些很好的反例,但是它们实在是太少了。