提问者:小点点

使用默认构造函数强制成员的未初始化声明


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

#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中,如果您像这样定义一个成员:memberi;,并且您没有在每个构造函数中初始化它,那么在尝试使用它时,您将得到一个错误,说明字段可能未初始化。

如果我从member类中删除默认构造函数,我将得到我想要的行为-编译器强制您为每个构造函数使用初始值列表-但我希望这种情况通常发生,以防止我忘记使用此表单(当默认构造函数可用时)。

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

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

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

一个相关的例子是:

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

您可以简单地这样做:std::strings=“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的默认构造函数的代价相对较低,但这并不是适用于所有情况。

我不想缺省构造函数消失,我只想要一个选项,让成员在构造函数出现之前未初始化--就像我可以处理没有缺省构造函数的类型一样。 对我来说,这似乎是一个如此简单的特性,但我对为什么不支持它感到困惑

如果不是支持类的无括号实例化,这似乎很自然地就会实现(每当没有默认构造函数的未初始化类型声明时),因为类的无括号实例化会冒昧地实例化类--即使当您希望它们保持未初始化时,就像我的情况一样。


共3个答案

匿名用户

匿名用户

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

匿名用户

一个解决方案是提供一个简单的通用包装器来防止默认构造,同时允许所有其他用例。 不必太多; 例如,像这样天真的方法应该可以很好地完成任务。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; }
};

如果我们在对象中使用它。。。

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

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

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

。。。我们现在需要显式初始化初始值设定项列表中的member,这是因为原始的object默认构造函数对decltype(member)()的隐式调用通过nodefaultconstructville的deleted后巷被发送。

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