首页 > 解决方案 > 使用默认构造函数强制未初始化的成员声明

问题描述

我今天发现了这种现象,其中一个成员被不必要地构造了两次:

#include <iostream>

class Member {
public:
    Member() {
        std::cout << "Created member (default)" << std::endl;
    }

    Member(int i) {
        std::cout << "Created member: " << i << std::endl;
    }
};

class Object {
    Member member;

public:
    Object() {
        member = 1;
    }
};

int main() {
    Object o;
    return 0;
}

有没有办法声明成员未初始化- 而不是使用默认构造函数 - 从而迫使您在构造函数中使用初始化列表?

在Java中,如果你像这样定义一个成员:Member i;并且你没有在每个构造函数中初始化它,你会在尝试使用它时得到一个错误,说该字段可能未初始化。

如果我从类中删除默认构造函数Member,我会得到我想要的行为——编译器会强制你为每个构造函数使用一个初始化列表——但我希望这通常发生,以防止我忘记使用这种形式(当默认构造函数可用时)。


本质上,我想防止错误地使用默认构造函数,但看起来这不存在......

explicit即使用关键字标记构造函数,Member member仍然会生成一个成员 - 当它在构造函数中重新分配时会立即被丢弃。这本身似乎也不一致......

我的主要问题是不一致。如果没有默认构造函数,您可以声明未初始化的成员;这实际上很有用;您不需要提供初始冗余声明,而只需在构造函数中初始化(如果未初始化则中断)。具有默认构造函数的类完全缺少此功能。


一个相关的例子是:

std::string s;
s = "foo"; 

您可以简单地这样做:std::string s = "foo";相反,如果"foo"实际上是多行 - 而不是单个表达式 - 我们会得到非原子初始化。

std::string s = "";
for (int i = 0; i < 10; i++) s += i;

这种初始化很容易以撕裂的方式结束。

如果像这样拆分它,它几乎是原子分配的,但是您仍然可以将默认值用作占位符:

std::string member;
// ...
std::string s = "";
for (int i = 0; i < 10; i++) s += i;
member = s; 

在这段代码中,您实际上可以在完全构造member后简单地将变量向下移动;s然而,在一个类中,这是不可能的,因为具有默认构造函数的成员必须在 decleration 时初始化——尽管没有默认构造函数的成员不受相同方式的限制。

在上述情况下,冗余使用std::string' 的默认构造函数相对便宜,但这并不适用于所有情况。


我不希望默认构造函数消失,我只想要一个选项让成员在构造函数之前保持未初始化 - 就像我可以使用没有默认构造函数的类型一样。对我来说,这似乎是一个如此简单的功能,我对为什么不支持它感到困惑/

如果不是支持无括号的类的实例化,这似乎自然会实现(只要没有默认构造函数的类型的未初始化声明),它假定实例化类 - 即使您希望它们未初始化,就像我的情况一样。


编辑:再次遇到这个问题

在java中你可以这样做

int x; // UNINITIALISED
if (condition){
   x = 1; // init x;
}
else return;
use(x); // INITIALISED

在 C++ 中这是不可能的???它使用默认构造函数进行初始化,但这不是必需的——它很浪费。 - 注意:你不能使用未初始化的变量。正如你所看到的,因为我x在循环之外使用,它必须在那里声明,此时它 - 不必要地 - 初始化。int x = delete另一个有用的场景。它不会破坏任何代码,并且只会在尝试使用未初始化的 x 时导致编译时错误。 没有未初始化的内存或不确定的状态,它只是编译时的事情——Java 已经能够很好地实现。

标签: c++

解决方案


重要的是要记住 C++ 不是 Java。在 C++ 中,变量是对象,而不是对对象的引用。当你在 C++ 中创建一个对象时,你就创建了一个对象。调用默认构造函数来创建对象与调用任何其他构造函数一样有效。在 C++ 中,一旦进入类构造函数的主体,它的所有成员子对象都是完整的对象(至少,就语言而言)。

如果某些类型具有默认构造函数,则意味着您可以 100% 使用该默认构造函数来创建该类型的实例。这样的对象不是“未初始化的”;它通过其默认构造函数进行初始化。

简而言之,您认为默认构造的对象“未初始化”或以其他方式无效是错误的除非该默认构造函数显式地使对象处于非功能状态,否则不会。

我不希望默认构造函数消失,我只想要一个选项让成员在构造函数之前保持未初始化 - 就像我可以使用没有默认构造函数的类型一样。

同样,C++ 不是 Java。C++ 中的“未初始化”一词的含义与处理 Java 时完全不同。

Java 声明引用,C++ 声明对象(和引用,但它们必须立即绑定)。如果一个对象“未初始化”,它仍然是C++ 中的一个对象。该对象具有未定义的值,因此您访问它的方式受到限制。但就 C++ 的对象模型而言,它仍然是一个完整的对象。你不能在以后构建它(不是没有placement-new)。

在 Java 中,未初始化变量意味着没有对象;这是一个空引用。C++ 没有等效的语言概念,除非所讨论的成员是指向对象的指针而不是对象本身。这是一个相当重量级的操作。

无论如何,在 C++ 中,类的作者有权限制该类的工作方式。这包括它是如何被初始化的。如果一个类的作者想要确保该对象中的某些值始终被初始化,那么他们就可以这样做,而无法阻止它。

一般来说,你应该避免尝试做你正在做的事情。但是,如果您必须在构造函数成员初始值设定项列表之外初始化某些类型,并且您不想调用其默认构造函数(或者它没有),那么您可以使用std::optional<T>,有T问题的类型在哪里. optional听起来是这样的:一个可能持有也可能不持有T. 它的默认构造函数以不带 . 开头T,但您可以T使用optional::emplace. 您可以访问Twith 指针语法,如->or *。但它从不堆分配T,所以你没有那个开销。


推荐阅读