ChatGPT解决这个技术问题 Extra ChatGPT

Convenient C++ struct initialisation

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.

I think the B example is as close as you are going to get.
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.
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.
Plus, if the definition of FooBar were to change in the future, the initialization could become broken.
if initialization gets long and complex, don't forget about the builder pattern

i
iammilind

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.


+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.
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.
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-extensions
@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.
@iammilind 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.
i
ivaigult

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.


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.
@Catskul No. It will be initialized with empty initializer list, which will result into initialization with zero.
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.
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.
20201 ? Golly gee I took a loooong nap!
e
eyelash

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.


Such approach initializes fields twice. Once in constructor. Second is fb.XXX = YYY.
r
ralphtheninja

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

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.
M
Matthieu M.

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.


"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.
@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.
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).
M
Matthias Urlichs

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


Which is not compliant to the C++ standard!
I know it's non-standard. But if you can use it, it's still the most sensible way to initialize a struct.
You can protect types of x and y making wrong constructor private: private: FooBar(float x, int y) {};
clang (llvm based c++ compiler) also supports this syntax. Too bad it's not part of the standard.
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.
p
parapura rajkumar

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

Cumbersome for functional programming (i.e. creating the object in the argument list of a function call), but really a neat idea otherwise!
the optimizer probably reduces it, but my eyes don't.
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?
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 site
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!
J
John

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.


Instead of memset you can use FooBar fb = {}; in C++, it default-initializes all struct members.
@ÖöTiib: Unfortunately that's illegal C, though.
M
Max Vollmer

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);\

Is there a C++20 proposal to allow the named initializers?
Ö
Öö Tiib

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.


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.
@bitmask 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.
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.
@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.
@ChristianRau Since when is C99 not C? Isn't C the group and C99 a particular version/ISO specification?
c
cls

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.


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++?
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.
I don't think this work. Try ABCD abc = { abc.b = 7, abc.a = 5 };.
@deselect, it works because field is initialized with value, returned by operator=. So, actually you initialize class member twice.
E
Emanuele Pavanello

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.


N
Nelson

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.


u
user3333852

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.


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 Review