Convenient C++ struct initialisation [duplicate]
Asked Answered
S

14

173

I'm trying to find a convenient way to initialise 'pod' C++ structs. Now, consider the following struct:

struct FooBar {
  int foo;
  float bar;
};
// just to make all examples work in C and C++:
typedef struct FooBar FooBar;

If I want to conveniently initialise this in C (!), I could simply write:

/* A */ FooBar fb = { .foo = 12, .bar = 3.4 }; // illegal C++, legal C

Note that I want to explicitly avoid the following notation, because it strikes me as being made to break my neck if I change anything in the struct in the future:

/* B */ FooBar fb = { 12, 3.4 }; // legal C++, legal C, bad style?

To achieve the same (or at least similar) in C++ as in the /* A */ example, I would have to implement an annoying constructor:

FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) {}
// ->
/* C */ FooBar fb(12, 3.4);

Which feels redundant and unnecessary. Also, it is pretty much as bad as the /* B */ example, as it does not explicitly state which value goes to which member.

So, my question is basically how I can achieve something similar to /* A */ or better in C++? Alternatively, I would be okay with an explanation why I should not want to do this (i.e. why my mental paradigm is bad).

EDIT

By convenient, I mean also maintainable and non-redundant.

Starryeyed answered 30/5, 2011 at 23:59 Comment(7)
I think the B example is as close as you are going to get.Lymn
I don't see how example B is "bad style." It makes sense to me, since you're initializing each member in turn with their respective values.Terrorist
Mike, it's bad style because it is not clear which value goes to which member. You have to go and look at the definition of the struct and then count members to find what each value means.Howdy
Plus, if the definition of FooBar were to change in the future, the initialization could become broken.Unmade
if initialization gets long and complex, don't forget about the builder patternAcapulco
I've actually used the "A" style in my C++ project and it worked fine with GCC, then I tried to build it with MSVC and it didn't quite like it. So you can use it as long as you don't mind the non-cross-compiler-ness.Spile
To achieve what you want, call your local Congressman and ask him to vote for Proposal P0329R3 during the next C++ Standards Committee meeting.Intercession
B
50

Designated initializes will be supported in c++2a, but you don't have to wait, because they are officialy supported by GCC, Clang and MSVC.

#include <iostream>
#include <filesystem>

struct hello_world {
    const char* hello;
    const char* world;
};

int main () 
{
    hello_world hw = {
        .hello = "hello, ",
        .world = "world!"
    };
    
    std::cout << hw.hello << hw.world << std::endl;
    return 0;
}

GCC Demo MSVC Demo

Update 2021

As @Code Doggo noted, anyone who is using Visual Studio 2019 will need to set /std:c++latest  for the "C++ Language Standard" field contained under Configuration Properties -> C/C++ -> Language.

Bailor answered 23/8, 2018 at 15:39 Comment(5)
Caveat emptor: keep in mind that if you add parameters to the end of the struct later, old initializations will still silently compile without having been initialized.Gilburt
@Gilburt No. It will be initialized with empty initializer list, which will result into initialization with zero.Bailor
You're right. Thank you. I should clarify, the remaining parameters will silently be effectively default initialized. The point I meant to make was that anyone hoping this might help enforce complete explicit initialization of POD types will be disappointed.Gilburt
As of Dec 31st, 2020, anyone who is using Visual Studio 2019 will need to set /std:c++latest  for the "C++ Language Standard" field contained under Configuration Properties -> C/C++ -> Language. This will provide access to the C++20 features currently available under development. C++20 is not available as complete and finalized implementation for Visual Studio yet.Sligo
20201 ? Golly gee I took a loooong nap!Unintentional
A
47

Since style A is not allowed in C++ and you don't want style B then how about using style BX:

FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 };  // :)

At least help at some extent.

Adelaidaadelaide answered 31/5, 2011 at 3:6 Comment(7)
+1: it does not really ensure correct initialization (from the compiler POV) but sure helps the reader... although the comments ought to be kept in sync.Antoninaantonino
Comment doesn't prevent initialization of the structure from being broken if I insert new field between foo and bar in the future. C would still initialize the fields we want, but C++ would not. And this is the point of the question - how to achieve the same result in C++. I mean, Python does this with named arguments, C - with "named" fields, and C++ should have something too, I hope.Galvan
Comments in sync? Give me a break. Safety goes through the window. Reorder the parameters and boom. Much better with explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Note the explicit keyword. Even breaking the standard is better in regards to safety. In Clang: -Wno-c99-extensionsWhatnot
@DanielW, It's not about what is better or what is not. this answer in accordance that the OP doesn't want Style A (not c++), B or C, which covers all the valid cases.Adelaidaadelaide
@Adelaidaadelaide I think a hint as to why OP's mental paradigm is bad could improve the answer. I consider this dangerous as it is now.Tomkins
@Adelaidaadelaide why are we even considering the = here? There's no need of that!Galiot
@DanielO that is a pointless when you are trying to do static initialization and code can't run yet ffs!Jacquie
F
13

You could use a lambda:

const FooBar fb = [&] {
    FooBar fb;
    fb.foo = 12;
    fb.bar = 3.4;
    return fb;
}();

More information on this idiom can be found on Herb Sutter's blog.

Furman answered 18/3, 2018 at 10:58 Comment(3)
Such approach initializes fields twice. Once in constructor. Second is fb.XXX = YYY.Jackpot
@DmytroOvdiienko No, copy elision will prevent that from occurring: en.cppreference.com/w/cpp/language/copy_elisionRandirandie
@Randirandie I meant that fields might be initialized at first in the default ctor and then in the fb.XXX = YYY expression. But as author stated that it is POD type, so that is not a case anymore.Jackpot
P
9

Extract the contants into functions that describe them (basic refactoring):

FooBar fb = { foo(), bar() };

I know that style is very close to the one you didn't want to use, but it enables easier replacement of the constant values and also explain them (thus not needing to edit comments), if they ever change that is.

Another thing you could do (since you are lazy) is to make the constructor inline, so you don't have to type as much (removing "Foobar::" and time spent switching between h and cpp file):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};
Prohibition answered 31/5, 2011 at 7:51 Comment(1)
I highly recommend anyone else reading this question to choose the style in the bottom code-snippet for this answer if all you're looking to do is be able to quickly initialize structs with a set of values.Tommi
A
8

Your question is somewhat difficult because even the function:

static FooBar MakeFooBar(int foo, float bar);

may be called as:

FooBar fb = MakeFooBar(3.4, 5);

because of the promotion and conversions rules for built-in numeric types. (C has never been really strongly typed)

In C++, what you want is achievable, though with the help of templates and static assertions:

template <typename Integer, typename Real>
FooBar MakeFooBar(Integer foo, Real bar) {
  static_assert(std::is_same<Integer, int>::value, "foo should be of type int");
  static_assert(std::is_same<Real, float>::value, "bar should be of type float");
  return { foo, bar };
}

In C, you may name the parameters, but you'll never get further.

On the other hand, if all you want is named parameters, then you write a lot of cumbersome code:

struct FooBarMaker {
  FooBarMaker(int f): _f(f) {}
  FooBar Bar(float b) const { return FooBar(_f, b); }
  int _f;
};

static FooBarMaker Foo(int f) { return FooBarMaker(f); }

// Usage
FooBar fb = Foo(5).Bar(3.4);

And you can pepper in type promotion protection if you like.

Antoninaantonino answered 31/5, 2011 at 6:52 Comment(3)
"In C++, what you want is achievable": wasn't OP asking to help prevent the mixup of the order of parameters? How does the template you propose would achieve that? Just for simplicity, let's say we have 2 parameters, both of them int.Intercession
@max: It will prevent it only if the types differ (even if they are convertible to each other), which is the OP situation. If it cannot distinguish the types, then of course it doesn't work, but that's a whole other question.Antoninaantonino
Ah got it. Yeah, these are two different problems, and I guess the second one doesn't have a good solution in C++ at the moment (but it appears C++ 20 is adding the support for the C99-style parameter names in the aggregate initialization).Intercession
V
6

Many compilers' C++ frontends (including GCC and clang) understand C initializer syntax. If you can, simply use that method.

Viguerie answered 31/12, 2012 at 12:23 Comment(6)
Which is not compliant to the C++ standard!Starryeyed
I know it's non-standard. But if you can use it, it's still the most sensible way to initialize a struct.Viguerie
You can protect types of x and y making wrong constructor private: private: FooBar(float x, int y) {};Galvan
clang (llvm based c++ compiler) also supports this syntax. Too bad it's not part of the standard.Gourmand
We all know that C initializers are not part of the C++ standard. But many compilers do understand it and the question didn't say which compiler is being targeted, if any. Thus please don't downvote this answer.Viguerie
Addendum: they will be part of the standard in C++20. So just wait a few more years. ;-)Viguerie
B
4

Yet another way in C++ is

struct Point
{
private:

 int x;
 int y;

public:
    Point& setX(int xIn) { x = Xin; return *this;}
    Point& setY(int yIn) { y = Yin; return *this;}

}

Point pt;
pt.setX(20).setY(20);
Baxie answered 31/5, 2011 at 0:17 Comment(7)
Cumbersome for functional programming (i.e. creating the object in the argument list of a function call), but really a neat idea otherwise!Starryeyed
I seriously doubt any good optimizer doesn't reduce it both to equivalent statementsBaxie
the optimizer probably reduces it, but my eyes don't.Antoninaantonino
As an example it is not that good, as most people will realize that Point pt(20, 20); will set the coordinates of the point. If the first parameter is y and the second is x that's a design error and not a problem with the initialization syntax. :-)Inference
Two words: argh...argh! How is this better than using public data with 'Point pt; pt.x = pt.y = 20;`? Or if you want encapsulation, how is this better than a constructor?Dripping
It is better than a constructor because you have to look at the constructor declaration for the parameter order ... is it x , y or y , x but the way I have showed it is evident at call siteBaxie
This does not work if you want a const struct. or if you want to tell the compiler not to allow uninitialized structs. If you really want to do it this way, at least mark the setters with inline!Viguerie
M
3

Option D:

FooBar FooBarMake(int foo, float bar)

Legal C, legal C++. Easily optimizable for PODs. Of course there are no named arguments, but this is like all C++. If you want named arguments, Objective C should be better choice.

Option E:

FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

Legal C, legal C++. Named arguments.

Marijo answered 31/5, 2011 at 0:22 Comment(2)
Instead of memset you can use FooBar fb = {}; in C++, it default-initializes all struct members.Anaglyph
@ÖöTiib: Unfortunately that's illegal C, though.Dianoetic
S
3

I know this question is old, but there is a way to solve this until C++20 finally brings this feature from C to C++. What you can do to solve this is use preprocessor macros with static_asserts to check your initialization is valid. (I know macros are generally bad, but here I don't see another way.) See example code below:

#define INVALID_STRUCT_ERROR "Instantiation of struct failed: Type, order or number of attributes is wrong."

#define CREATE_STRUCT_1(type, identifier, m_1, p_1) \
{ p_1 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_2(type, identifier, m_1, p_1, m_2, p_2) \
{ p_1, p_2 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_4(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3, m_4, p_4) \
{ p_1, p_2, p_3, p_4 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_4) >= (offsetof(type, m_3) + sizeof(identifier.m_3)), INVALID_STRUCT_ERROR);\

// Create more macros for structs with more attributes...

Then when you have a struct with const attributes, you can do this:

struct MyStruct
{
    const int attr1;
    const float attr2;
    const double attr3;
};

const MyStruct test = CREATE_STRUCT_3(MyStruct, test, attr1, 1, attr2, 2.f, attr3, 3.);

It's a bit inconvenient, because you need macros for every possible number of attributes and you need to repeat the type and name of your instance in the macro call. Also you cannot use the macro in a return statement, because the asserts come after the initialization.

But it does solve your problem: When you change the struct, the call will fail at compile-time.

If you use C++17, you can even make these macros more strict by forcing the same types, e.g.:

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_1) == typeid(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_2) == typeid(identifier.m_2), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_3) == typeid(identifier.m_3), INVALID_STRUCT_ERROR);\
Stratfordonavon answered 10/1, 2018 at 15:1 Comment(2)
Is there a C++20 proposal to allow the named initializers?Estebanesteem
@MaëlNison Yes: Designated initializers (since C++20)Stratfordonavon
D
2

The way /* B */ is fine in C++ also the C++0x is going to extend the syntax so it is useful for C++ containers too. I do not understand why you call it bad style?

If you want to indicate parameters with names then you can use boost parameter library, but it may confuse someone unfamiliar with it.

Reordering struct members is like reordering function parameters, such refactoring may cause problems if you don't do it very carefully.

Drury answered 31/5, 2011 at 0:3 Comment(8)
I call it bad style because I think it is zero maintainable. What if I add another member in a year? Or if I change the ordering/types of the members? Every piece of code initialising it might (very likely) break.Starryeyed
@Starryeyed But as long as you do not have named arguments, you would have to update constructor calls, too and I think not many people think constructors are unmaintainable bad style. I also think named initialization is not C, but C99, of which C++ is definitely not a superset.Karisa
If you add another member in a year to end of the struct then it will be default-initialized in already existing code. If you reorder them then you have to edit all existing code, nothing to do.Anaglyph
@bitmask: The first example would be "unmaintainable" as well then. What happens if you rename a variable in the struct instead? Sure, you could do a replace-all, but that could accidentally rename a variable that shouldn't be renamed.Terrorist
@ChristianRau Since when is C99 not C? Isn't C the group and C99 a particular version/ISO specification?Jiggered
@Jiggered Yeah, of course C99 is C. But this isn't relevant to the question at all, since this particular C99-feature (named initialization) is not part of the C subset included in C++, not even of the C99 parts included in C++11. But that comment might have been worded not that accurately, I agree.Karisa
@ÖöTiib If you add another member in a year to end of the struct then it will be default-initialized in already existing code. - which is even worse, because now I have to go through every initialisation and count number of arguments to make sure that I did not forgot to add the new value.Atlantes
@DmitryZaitsev Worse than what? If maintenance modifies layout of data then they have to go through every initialization. If for nothing else then just to verify that the /* A */ was always used since /* B */ is also legal C.Anaglyph
U
1

What about this syntax?

typedef struct
{
    int a;
    short b;
}
ABCD;

ABCD abc = { abc.a = 5, abc.b = 7 };

Just tested on a Microsoft Visual C++ 2015 and on g++ 6.0.2. Working OK.
You can make a specific macro also if you want to avoid duplicating variable name.

Utas answered 30/11, 2016 at 14:33 Comment(4)
clang++ 3.5.0-10 with -Weverything -std=c++1z seems to confirm that. But it doesn't look right. Do you know where the standard confirms that this is valid C++?Starryeyed
I do not know, but I've used that in different compilers since long time ago and did not see any problems. Now tested on g++ 4.4.7 - works fine.Utas
I don't think this work. Try ABCD abc = { abc.b = 7, abc.a = 5 };.Lustrous
@deselect, it works because field is initialized with value, returned by operator=. So, actually you initialize class member twice.Jackpot
O
1

For me the laziest way to allow inline inizialization is use this macro.

#define METHOD_MEMBER(TYPE, NAME, CLASS) \
CLASS &set_ ## NAME(const TYPE &_val) { NAME = _val; return *this; } \
TYPE NAME;

struct foo {
    METHOD_MEMBER(string, attr1, foo)
    METHOD_MEMBER(int, attr2, foo)
    METHOD_MEMBER(double, attr3, foo)
};

// inline usage
foo test = foo().set_attr1("hi").set_attr2(22).set_attr3(3.14);

That macro create attribute and self reference method.

Optime answered 19/10, 2018 at 12:48 Comment(1)
this approach ins't applicable if you can't change the struct's declaration (when it, for instance, resides in a third-party library)Enjoy
P
0

For versions of C++ prior to C++20 (which introduces the named initialization, making your option A valid in C++), consider the following:

int main()
{
    struct TFoo { int val; };
    struct TBar { float val; };

    struct FooBar {
        TFoo foo;
        TBar bar;
    };

    FooBar mystruct = { TFoo{12}, TBar{3.4} };

    std::cout << "foo = " << mystruct.foo.val << " bar = " << mystruct.bar.val << std::endl;

}

Note that if you try to initialize the struct with FooBar mystruct = { TFoo{12}, TFoo{3.4} }; you will get a compilation error.

The downside is that you have to create one additional struct for each variable inside your main struct, and also you have to use the inner value with mystruct.foo.val. But on the other hand, it`s clean, simple, pure and standard.

Provincialism answered 12/8, 2020 at 20:26 Comment(0)
D
-1

I personally have found that using constructor with struct is the most pragmatic way to ensure struct members are initialized in code to sensible values.

As you say above, small downside is that one does not immediatelly see what param is which member, but most IDEs help here, if one hovers over the code.

What I consider more likely is that new member is added and in this case i want all constructions of the struct to fail to compile, so developer is forced to review. In our fairly large code base, this has proven itself, because it guides developer in what needs attention and therefore creates self-maintained code.

Dropsonde answered 6/2, 2022 at 9:4 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewHackbut

© 2022 - 2024 — McMap. All rights reserved.