是否可以防止省略聚合初始化成员?


43

我有一个结构,其中有许多相同类型的成员,像这样

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

问题是,如果我忘了初始化一个结构成员(例如wasactive),就像这样:

VariablePointers{activePtr, filename}

编译器不会抱怨它,但是我将拥有一个部分初始化的对象。如何防止这种错误?我可以添加一个构造函数,但是它将重复两次变量列表,所以我必须键入所有这三次!

如果有针对C ++ 11的解决方案,请同时添加C ++ 11的答案(当前我仅限于该版本)。不过,也欢迎使用最新的语言标准!


6
键入构造函数听起来并不那么糟糕。除非您有太多的成员,在这种情况下,也许重构是适当的。
Gonen I

1
@Someprogrammerdude我认为他的意思是错误的是,您可能会意外忽略初始化值
Gonen I

2
@theWiseBro,如果您知道数组/向量如何帮助您应该发布答案。它不是那么明显,我看不到
idclev 463035818

2
@Someprogrammerdude但这甚至是警告吗?在VS2019中看不到它。
acraig5075

8
有一个-Wmissing-field-initializers编译标志。
罗恩

Answers:


42

这是一个技巧,如果缺少必需的初始化程序,它将触发链接器错误:

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

用法:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

结果:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

注意事项:

  • 在C ++ 14之前,这Foo根本无法成为一个集合。
  • 从技术上讲,这依赖于未定义的行为(违反了ODR),但是应该在任何理智的平台上都可以工作。

您可以删除转换运算符,然后是编译器错误。
jrok

@jrok是的,但是Foo即使您从未真正调用过运算符,它也会在声明后立即发出。
昆汀

2
@jrok但是即使提供了初始化,它也不会编译。godbolt.org/z/yHZNq_ 附录:对于MSVC,它的工作方式如您所述:godbolt.org/z/uQSvDa这是一个错误吗?
n314159

当然,傻我。
jrok

6
不幸的是,此技巧不适用于C ++ 11,因为它将变成非聚合的:(我删除了C ++ 11标记,因此您的答案也可行(请不要删除它),但是如果可能的话,仍然首选C ++ 11解决方案
Johannes Schaub-litb

22

对于clang和gcc,可以进行编译-Werror=missing-field-initializers,将缺少字段初始化程序的警告转换为错误。哥德宝

编辑:对于MSVC,即使在level上也似乎没有发出警告/Wall,因此我认为无法使用此编译器警告缺少的初始化程序。哥德宝


7

我想这不是一个方便的优雅解决方案,但是应该也可以与C ++ 11一起使用,并给出编译时(而不是链接时)错误。

这个想法是在结构中的最后一个位置添加一个没有默认初始化的类型的其他成员(并且该成员不能使用type的值VariablePtr(或任何先前值的类型)进行初始化)

举个例子

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

这样,您被迫在聚合初始化列表中添加所有元素,包括显式初始化最后一个值的值(sentinel在示例中为的整数),否则将收到“调用已删除的'bar'构造函数”错误。

所以

foo f1 {'a', 'b', 'c', 1};

编译并

foo f2 {'a', 'b'};  // ERROR

没有。

不幸的是也

foo f3 {'a', 'b', 'c'};  // ERROR

无法编译。

-编辑-

正如MSalters(感谢)指出的那样,在我的原始示例中有一个缺陷(另一个缺陷):bar可以使用一个char值(可以转换为int)来初始化一个值,因此可以进行以下初始化

foo f4 {'a', 'b', 'c', 'd'};

这可能会非常令人困惑。

为避免此问题,我添加了以下已删除的模板构造函数

 template <typename T> 
 bar (T const &) = delete;

所以前面的f4声明给出了编译错误,因为该d值被删除的模板构造函数拦截


谢谢,这很好!正如您所提到的那样,它并不完美,并且也foo f;无法编译,但这也许更多的是功能而不是此技巧的缺陷。如果没有比这更好的建议,将接受。
约翰内斯·绍布

1
我将使bar构造函数接受一个名为init_list_end之类的const嵌套类成员以提高可读性
Gonen I

@GonenI-为便于阅读,您可以接受enum,并init_list_end(简单地list_end)命名一个值enum;但是可读性增加了很多打字操作,因此,鉴于附加值是此答案的薄弱环节,我不知道这是一个好主意。
max66

也许constexpr static int eol = 0;在的标题中添加类似的内容bartest{a, b, c, eol}对我来说似乎很可读。
n314159

@ n314159-好吧...成为bar::eol; 几乎是通过enum值;但我认为这并不重要:答案的核心是“在结构的最后位置添加一个没有默认初始化的类型的其他成员”;该bar部分只是一个简单的例子,说明该解决方案有效。确切的“没有默认初始化的类型”应视情况而定(IMHO)。
max66

4

对于CppCoreCheck,有一条规则进行检查,即是否所有成员都已初始化,并且可以将其从警告转换为错误-这通常是在整个程序范围内进行的。

更新:

您要检查的规则是typesafety的一部分Type.6

Type.6:始终初始化成员变量:始终初始化,可能使用默认构造函数或默认成员初始化程序。


2

最简单的方法是不为成员类型提供无参数的构造函数:

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

另一种选择:如果您的成员是const&,则必须初始化所有成员:

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

如果您可以与一个虚拟const&成员一起生活,则可以将其与@ max66的定点概念结合起来。

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

来自cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

如果初始化程序子句的数量少于成员数量,或者初始化程序列表完全为空,则其余成员将被值初始化。如果引用类型的成员是这些其余成员之一,则程序格式错误。

另一种选择是采用max66的哨兵概念,并添加一些语法糖以提高可读性

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK

不幸的是,这使它变得A不可移动并改变了复制语义(A可以说不再是值的总和):(
Johannes Schaub-litb

@ JohannesSchaub-litb好。我编辑后的答案中的这个想法怎么样?
Gonen I

@ JohannesSchaub-litb:同样重要的是,第一个版本通过使成员成为指针来增加一个间接级别。更重要的是,它们必须是某些事物的引用,并且1,2,3对象实际上是自动存储中的局部变量,当函数结束时,它们超出范围。在具有64位指针的系统(例如x86-64)上,它使sizeof(A)为24,而不是3。
Peter Cordes

虚拟引用将大小从3字节增加到16个字节(填充以填充指针(引用)成员+指针本身。)只要您从不使用引用,就可以指向一个超出范围的对象,范围。我当然会担心它不会优化,而将其复制肯定不会。(空类的大小可能比其他类更好,因此,这里的第三种选择最不坏,但至少在某些ABI中它仍然占用了每个对象的空间。我仍然还要担心填充会造成伤害某些情况下进行优化。)
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.