Safer way to maintain struct initializations

I ran into this problem a bit ago, and was wondering what future-proofing refactor people would suggest.

Let's say I have a struct that just contains some simple configuration info.
It's initialized in the way you see below.
1
2
3
4
5
6
7
8
9
struct Foo {
    float value;
    bool toggle;
};

const Foo foo = {
    0.0f,
    true
};


Everything works just fine and dandy. But later on, another developer adds a new fie- I mean member variable to the struct. The following code surprising still compiles because the compiler allows an implicit cast from bool to float. Eww.

What alternative way would you set this up to prevent adding a field from causing a runtime error? I want a compiler error to happen at the place where we attempt to create an outdated version of the struct.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

struct Foo {
    float value;
    float value_extra; // value added later in development
    bool toggle;
};
    
int main()
{
    // old code! never updated for "value_extra"
    const Foo foo = {
        0.0f,
        true
    };
    
    std::cout << "value_A: " << foo.value << '\n'
              << "value_extra: " << foo.value_extra << '\n'
              << "toggle: " << foo.toggle << '\n';
}


output:
value_A: 0
value_extra: 1
toggle: 0 
Last edited on
Hi

My first thought was this:

1
2
3
4
5
// old code! never updated for "value_extra"
    const Foo foo = {
        {0.0f},
        {true}
    };


This would at least give a compile time warning because the conversion of {true} to float is narrowing. I often put the braces around each function argument for added protection.

Maybe try structured bindings? The number of bindings must match the number of non static members of the struct.

Also try type traits, like std::is_same.

https://en.cppreference.com/w/cpp/types/is_same

Also, try using strong types. They will definitely cause compilation problems if one does the wrong thing, but the flip side is the embuggerance* TM of implementing them in an existing code base.Another advantage of strong types is that one can explicitly control what types can be converted.

* read: nuisance value

I often wish that that the explicit keyword could apply to all functions, not just constructors.
C++20 to the rescue.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

struct Foo {
    float value;
    bool toggle;
};

const Foo foo = {
    .value = 0.0f,
    .toggle = true
};

int main() {
    std::cout << foo.value << " " << foo.toggle << std::endl;
}
You can default initialise within Foo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Foo {
	float value { 2.2 };
	float value_extra {}; // value added later in development
	bool toggle { true };
};

int main() {
	const Foo foo {};

	std::cout << "value_A: " << foo.value << '\n'
		<< "value_extra: " << foo.value_extra << '\n'
		<< "toggle: " << foo.toggle << '\n';
}


Also, struct is just a class with public members. so it can have constructors. Consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

struct Foo {
	Foo() {}
	Foo(float val, bool tog) : value(val), toggle(tog) {}

	float value { };
	float value_extra { }; // value added later in development
	bool toggle { };
};

int main() {
	const Foo foo {3.3, true};

	std::cout << "value_A: " << foo.value << '\n'
		<< "value_extra: " << foo.value_extra << '\n'
		<< "toggle: " << foo.toggle << '\n';
}


Thanks for the suggestions! I'll try them out. salem c, aggregate initialization might be the simplest thing w/o adding more boilerplate to the struct code itself, and breaking other parts of the code. seeplus, unfortunately making the constructor not implicitly defined breaks some aggregate struct initializations and the compiler isn't fully C++11 compliant, so that's unfortunate -- it appears to work fine in Visual Studio. TheIdeasMan, definitely changing the types to be stronger would produce a compiler error like I wanted, so maybe that's a path forward.
Maybe you want to take a look at the "pimpl" (pointer to implementation) pattern:
https://learn.microsoft.com/en-us/cpp/cpp/pimpl-for-compile-time-encapsulation-modern-cpp?view=msvc-170

This way, you can completely hide the implementation details from the "public" header file.

And, most important, even code that was compiled with an older version of the header file won't break, when you extend the type!

my_type.h
1
2
3
4
5
6
7
8
9
10
11
#include <memory>

struct MyType
{
	//  ... all public stuff goes here
	MyType();
	MyType(const float value, const bool toggle);
private:
	struct Impl;
	std::unique_ptr<Impl> p;
};

my_type.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "my_type.h"

struct MyType::Impl
{
	friend struct MyType;
protected:
	// ... all private data and internal functions
	bool toggle;
	float value;
	float value_extra; // <-- can now be added retrospectively, without anybody "outside" noticing!
};

MyType::MyType(const bool toggle, const float value) : p(new Impl)
{
	p->toggle = toggle;
	p->value = value;
	p->value_extra = 1.414215686; // <-- can now be added retrospectively, without anybody "outside" noticing!
}

MyType::MyType() : MyType(true, 0.42) { }
Last edited on
Registered users can post here. Sign in or register to post.