【问题标题】:Forcing uninitialised declaration of member with a default constructor使用默认构造函数强制未初始化的成员声明
【发布时间】:2020-06-13 00:27:05
【问题描述】:

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

#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;
}

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

在 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; 

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

在上述情况下,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++


【解决方案1】:

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

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

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

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

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

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

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

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

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

【讨论】:

【解决方案2】:

任何主流 C++ 编译器都没有这样的功能。我怎么知道?因为它基本上会破坏(或警告)每个现有的 C++ 库。您要求的内容不存在,而且在编译 C++ 的编译器中也不存在。

【讨论】:

  • 它不会破坏任何代码,未初始化的声明可以简单地选择加入。 IE。类似Member member = delete
  • 哦,我明白了。现在您要求将新功能添加到 C++ 语言中。您可以为适当的 C++ 工作组编写提案。但老实说,我认为这不会获得太多支持。从一种语言迁移到另一种语言总是有点棘手,人们倾向于期望新语言支持所有自己喜欢的习语....
  • @TobiAkinyemi 我不明白这如何解决您的问题。您说您无权访问相关课程。如果您有权访问该类以说出member member = delete,则您有权删除默认构造函数。
  • @Taekahn 你是什么意思?我可以访问Object(那里会出现未初始化的声明)-而不是Member
  • @TobiAkinyemi:在 C++ 中,它可以是正确的,并且对于默认构造某些东西然后分配或覆盖(至少部分)它是最佳的。它也更安全,因为只要输入构造函数的主体(在初始化列表之后),您就知道所有成员都是有效对象,而不是一些部分/不可用的对象。
【解决方案3】:

一种解决方案是提供一个简单的通用包装器,它可以防止默认构造,同时允许所有其他用例。不需要太多;例如,像这样幼稚的方法应该可以很好地完成任务。1

#include <utility> // std::forward()

template<typename T>
class NoDefaultConstruct {
    T data;

// All member functions are declared constexpr to preserve T's constexpr-ness, if applicable.
public:
    // Prevents NoDefaultConstruct<T> from being default-constructed.
    // Doesn't actually prevent T itself from being default-constructed, but renders T's
    //  default constructor inaccessible.
    constexpr NoDefaultConstruct() = delete;

    // Provides pass-through access to ALL of T's constructors, using perfect forwarding.
    // The deleted constructor above hides pass-through access to T's default constructor.
    template<typename... Ts>
    constexpr NoDefaultConstruct(Ts&&... ts) : data{std::forward<Ts>(ts)...} {}

    // Allow NoDefaultConstruct<T> to be implicitly converted to a reference to T, allowing
    //  it to be used as a T& in most constructs that want a T&.  Preserves const-ness.
    constexpr operator T&()       { return data; }
    constexpr operator T&() const { return data; }
};

如果我们在Object...中使用它...

class Object {
    //Member member;
    NoDefaultConstruct<Member> member;

public:
    // Error: Calls deleted function.
    //Object() {
    //    member = 1;
    //}

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

...我们现在需要在初始化器列表中显式初始化member,因为原始Object 默认构造函数对decltype(member)() 的隐式调用通过NoDefaultConstructville 的delete 绕道发送d 后巷。


1:请注意,虽然NoDefaultConstruct&lt;T&gt; 在大多数情况下的行为与T 或多或少相同,但也有例外。最引人注目的是在模板参数推导期间,以及使用模板参数推导规则的其他任何地方。

【讨论】:

  • Tbh 我发现这个版本比其他版本更难理解——可能是因为我是 C++ 新手
  • @TobiAkinyemi:只是让你知道,如果你大量使用这种东西,而另一个 C++ 开发人员查看你的代码,他们将无话可说。我也考虑在我的答案中编写这种包装器,但决定不这样做,因为它对于将要维护的 C++ 代码基本上是不切实际的。具有类似包装解决方案的其他答案也是如此。
  • 问题是 C++ 是否原生支持这个。
  • @Justin 如果我想有意调用它,这是否仍然支持使用默认构造函数?
  • 不幸的是,@TobiAkinyemi,使用类似这样的东西会禁用包装类型的默认构造函数。从技术上讲,它提供 通过模板构造函数访问T 的默认构造函数...但这是一个有争议的问题,因为如果您尝试调用它,将在重载解析期间选择已删除的构造函数(由于非模板函数在其他方面相同时比模板特化具有更高的优先级)。
【解决方案4】:

所以根据我们在 cmets 中的讨论,听起来这可能符合您的要求?
如前所述,您在 C++ 中寻找的确切内容不存在,但我认为有一些语言功能可以让您非常接近。

template <typename T>
struct must_init
{
    using type = std::remove_cvref_t<T>;
    type t;
    must_init(type&& t) : t{std::move(t)} {}
};

如果您在其中包装一个类,您将无法在不分配给它的情况下进行编译。 即

class A
{
    must_init<std::string> s;
};

会给你一个编译器错误,说 s 必须被初始化,而如果你像这样定义它

class A
{
    A() : s{""} {}
    must_init<std::string> s;
};

这将编译。 你可以像这样调用默认构造函数

class A
{
    A() : s{{}} {}
    must_init<std::string> s;
};

神箭。 https://godbolt.org/z/e_nsRQ

【讨论】:

  • 这是迄今为止我想要的最接近的东西
  • 双括号s{""}{}是怎么回事
  • 我们似乎都在几秒钟内提供了相同想法的变体。 ;P
  • 第二组大括号是构造函数的主体@TobiAkinyemi,它和s的初始化器之间没有空格。
  • @TobiAkinyemi 抱歉,这是一种习惯。 {} 是现代 C++ 中“首选”的初始化方式。 () 也适用于第一个大括号。 geeksforgeeks.org/uniform-initialization-in-c
【解决方案5】:

我也遇到过这个问题,因为我以前使用 Java 作为我的第一语言进行开发,并且出于个人原因我正在切换到 C++(我需要更低的访问级别)

在我的特殊情况下,我有一个sf::Thread 对象,我想在一个类中声明它,不对其进行初始化。由于这是 SFML 的一部分,我无法更改其实现。

在项目的不同部分,我想实际创建线程并启动它。

由于sf::Thread 未被实例化,我遇到了编译问题,因为当您将其声明为类的成员时,会自动调用构造函数。

经过一番研究,我发现了Smart Pointers,例如std::unique_ptr。此指针拥有并管理另一个对象。我发现它对我想要完成的事情很有用,缺点是你必须处理一个指针(所以你应该在完成后释放它)

// Somewhere (In your case in your main function, in my case it was a member of another class)
std::unique_ptr<sf::Thread> thread_background;

// In the calling method
void start_thread_background(){
    thread_background.reset(new sf::Thread(/*sf::Thread arguments*/));
    thread_background->launch(); // You must call methods with -> because it's a pointer now
}

【讨论】:

  • 也是用Java开始的,出于同样的原因切换
  • 使用堆分配、智能指针、包装器等作为延迟构造的方法是一种可行的解决方法,一些项目使用,但它不能解决问题,因为它们不会强制您初始化初始化列表(或某处)中的成员。
  • 或者,如果您必须延迟对象的初始化,您可以使用optional&lt;sf::Thread&gt;。您将避免不必要的堆分配,但代价是对象本身会膨胀。
【解决方案6】:

您遇到了两个故意的 C++ 设计决策,这会阻止您单独使用语言功能创建所需的工作流检查(当您可以使用其初始化程序列表时,您会被警告不要初始化构造函数主体中的成员) .

第一,C++ 的设计者决定制作它,因此不存在未初始化对象之类的东西。这就是为什么构造函数只能做两件事——创建一个函数对象,或者抛出一个异常。未初始化的类型会让人头疼(在 C++ 中,未初始化的整数经常会出现这种情况),因此设计者在编写语言规范时完全从对象中消除了这种状态。

第二,设计者还决定所有对象都应该有一个自动生成的默认构造函数,除非满足某些条件。 (例如,存在用户编写的默认构造函数,用户使用 =delete 语法删除默认构造函数,或者本身无法默认初始化的成员,例如引用类型。)

仅使用语言功能无法获得所需的内容,因此您可以使用 linter 等扩展程序获得所需的内容,也可以更改所需的内容。

我推荐后者,即适应典型的 C++ 做事方式。使用该语言时,它将减少您的摩擦。具体来说,当你真的想表达“没有额外信息就无法构造这个对象”时,我建议你删除默认构造函数,而在所有其他情况下,只需养成在构造函数初始化列表中初始化类成员的习惯。您希望通过这样做来遵循最佳实践,但不幸的是,没有直接的方法可以为自己建立一个护栏,您只需要注意并对自己执行规则即可。

当一个成员在构造函数主体中初始化时,可能会有一些 linter 生成警告,而它本可以在初始化器列表中初始化,但我个人不知道。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-24
    • 2016-04-05
    • 2015-07-03
    相关资源
    最近更新 更多