Looking at the examples (https://isocpp.org/files/papers/P2996R4.html#examples) what really stands out is the direct integration of type-syntax into the language. It fits in with a certain token-substitution way that connects back to templates. It also replaces some of the uglier operators (typeof?).
I hope it goes int! During the language's stagnation I left for a while, perhaps it'll be competitive again soon.
So it's not that we aren't getting features. They are coming quite fast and people regularly complain that new C++ has too many things for them to learn and keep up with. The issue is that those are the same features everyone has been asking for for over a decade so the people that really care found workarounds and eventually move over to the new std way of doing things when they can while everyone else continues waiting for that one feature they really care about.
1. how immense the language has become, and how hard it got to learn and implement
2. how "modernising" C++ gives developers less incentives to convince management to switch to safer languages
While I like C++ and how crazy powerful it is, I also must admit decades of using it that teaching it to new developers has become immensely hard in the last few years, and the "easier" inevitably ends up being the unsafe one (what else can you do when the language itself tells you to refrain from using `new`?).
Tree algorithms that are simple in literature get bloated and slow with shared_ptr.
The only issue with pointers in C++, which C does not have, is that so many things are copied around by default if one is using classes. So the way to deal with tree algorithms is to have a hidden tree with pointers and a class that wraps the tree and deletes all dangerous copy methods, implicit and explicit.
stdlib++ seems to use that approach as well.
It is just a different syntax for the same thing.
I honestly think people just find the words “unique ptr” scary or syntactically overwhelming. If that is true, fortunately we also have using aliases :)
There are all sorts of issues with pointers (and not just in C++) - which are inherent to their use. They can point anywhere! They're mutable! They can be null! It's difficult/impossible to ensure they hold a valid value!
Common wisdom is to avoid excessive use of pointers when you don't _really_ need - even smart pointers.
Consider this fine presentation for example:
"Don't use fking pointers" https://klmr.me/slides/modern-cpp/#1
Use references, especially as function parameters,
* Returning values in modern C++ typically does _not_ involve any copying.
* If you want to indicate the possibility that your value is uninitialized/invalid - use std::optional<T>, which can hold either an actual T value or an std::nullopt (being in an "empty" or "value-less" state).
* If your data is in some buffer (or span, or vector etc.), you can use offsets into that buffer.
* Many uses of pointers are due to the "need" to utilize polymorphism: myobj->foo() . Well, typically, you know the real type at compile-time, and can write a freestanding foo() function, which is polymorphic via overloading, or being templated over its parameter's type.
* And speaking of virtual methods and class hierarchies, you can often make do with a template parameter instead of a choice of subclass; or with an std::variant<Foo, Bar>
Even Go is rediscovering that staying simple just doesn't happen for any language that gets industry adoption at scale.
If you read the new "compilers" [1] in packages like PyTorch, which are unfortunately written in Python, you stare at a huge code base with walls of text, objects calling one another in a maze of ravioli code and generally no help at all to make sense of it all.
Compare that to the gcc code bases, where it is always possible to find some entry point to understand the whole thing.
[1] "compilers", because despite the huge code base (wasn't Python supposed to be terse?) they preprocess the function graph and then call g++ and Triton to do the actual work.
Indeed I wish they were even more aggressive about breaking changes
Rust is nifty but there is simply too much existing C/C++ out there and "rewrite it in Rust" is not a serious suggestion
Maybe one day we have some cool AI that magically rewrites old C/C++ automatically, but by then I also assume we will have AI-designed languages
Until then, we need C/C++ to be maintained and modernized because we are actually running the world with these languages
I bring it up partially because they are not taking a "rewrite it in Zig" approach, they are specifically aiming for incremental migration in mixed C / Zig codebases.
More specifically, with this I can iterate over struct members and get their names and types, but I cannot attach additional information to these members, like whether they should be serialized or under which name.
The referenced proposal P1887R1 covers this, but that's not included here, right?
P1887R1: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p18...
* What type of problems static reflection could solve, in general?
* Are there specific cases and / or situations where static reflection could resolve such case, even simplify an unnecessary complexity?* Converting enum values to strings, and vice versa
* Parsing command line arguments from a struct definition (like Rust's clap)
* Simple definition of tuple and variant types, without the complex metaprogramming tricks currently used
* Automatic conversion between struct-of-arrays and array-of-structs form
* A "universal formatter" that can print any struct with all its fields
* Hashing a struct by iterating over its fields
* Convert between a struct and tuple, tuple concatenation, named tuples
enum class Color { Red, Green, Blue };
template<typename E>
std::string enum_to_string(E value) {
constexpr auto enum_info = reflect(E);
for (const auto& enumerator : enum_info.enumerators()) {
if (enumerator.value() == value) {
return std::string(enumerator.name());
}
}
return "Unknown";
}
template<typename E>
E string_to_enum(const std::string& str) {
constexpr auto enum_info = reflect(E);
for (const auto& enumerator : enum_info.enumerators()) {
if (enumerator.name() == str) {
return enumerator.value();
}
}
throw std::invalid_argument("Invalid enum string");
}
Parsing command line arguments from a struct definition struct CLIOptions {
std::string input_file;
int num_threads = 1;
bool verbose = false;
};
template<typename T>
T parse_cli_args(int argc, char* argv[]) {
T options;
constexpr auto struct_info = reflect(T);
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
for (const auto& member : struct_info.members()) {
if (arg == "--" + std::string(member.name())) {
if (member.type() == typeid(bool)) {
member.set(options, true);
} else if (i + 1 < argc) {
member.set(options, std::string(argv[++i]));
}
break;
}
}
}
return options;
}
Simple definition of tuple and variant types // Common data structure used in examples below
struct Person {
std::string name;
int age;
double height;
};
// Tuple, without reflection
int main() {
std::tuple<std::string, int, double> person_tuple{"John Doe", 30, 175.5};
std::cout << "Name: " << std::get<0>(person_tuple) << std::endl;
std::cout << "Age: " << std::get<1>(person_tuple) << std::endl;
std::cout << "Height: " << std::get<2>(person_tuple) << std::endl;
Person p{"Jane Doe", 25, 165.0};
auto p_tuple = std::make_tuple(p.name, p.age, p.height);
return 0;
}
// Tuple, with reflection
int main() {
std::tuple<std::string, int, double> person_tuple{"John Doe", 30, 175.5};
std::apply([](const auto&... args) {
(..., (std::cout << reflect(args).name() << ": " << args << std::endl));
}, person_tuple);
Person p{"Jane Doe", 25, 165.0};
auto p_tuple = std::apply([&p](auto... members) {
return std::make_tuple(members.get(p)...);
}, reflect(Person).members());
return 0;
}
// Variant, without reflection
int main() {
std::variant<int, std::string, Person> var;
var = 42;
std::cout << "Variant holds: " << std::get<int>(var) << std::endl;
var = "Hello, World!";
std::cout << "Variant holds: " << std::get<std::string>(var) << std::endl;
var = Person{"Alice", 28, 170.0};
const auto& p = std::get<Person>(var);
std::cout << "Variant holds Person: " << p.name << ", " << p.age << ", " << p.height << std::endl;
std::visit([](const auto& v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "Int: " << v << std::endl;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "String: " << v << std::endl;
else if constexpr (std::is_same_v<T, Person>)
std::cout << "Person: " << v.name << std::endl;
}, var);
return 0;
}
// Variant, with reflection
int main() {
std::variant<int, std::string, Person> var;
var = 42;
std::cout << "Variant holds: " << std::get<int>(var) << std::endl;
var = "Hello, World!";
std::cout << "Variant holds: " << std::get<std::string>(var) << std::endl;
var = Person{"Alice", 28, 170.0};
std::visit([](const auto& v) {
constexpr auto type_info = reflect(std::decay_t<decltype(v)>);
std::cout << "Variant holds " << type_info.name() << ": ";
if constexpr (type_info.is_class()) {
for (const auto& member : type_info.members()) {
std::cout << member.name() << ": " << member.get(v) << ", ";
}
} else {
std::cout << v;
}
std::cout << std::endl;
}, var);
return 0;
}
Automatic conversion between struct-of-arrays and array-of-structs template<typename Struct, size_t N>
auto soa_to_aos(const StructOfArrays<Struct, N>& soa) {
std::array<Struct, N> aos;
constexpr auto struct_info = reflect(Struct);
for (size_t i = 0; i < N; ++i) {
for (const auto& member : struct_info.members()) {
member.set(aos[i], soa.get(member.name())[i]);
}
}
return aos;
}
template<typename Struct, size_t N>
auto aos_to_soa(const std::array<Struct, N>& aos) {
StructOfArrays<Struct, N> soa;
constexpr auto struct_info = reflect(Struct);
for (size_t i = 0; i < N; ++i) {
for (const auto& member : struct_info.members()) {
soa.get(member.name())[i] = member.get(aos[i]);
}
}
return soa;
}
Universal formatter: template<typename T>
std::string format(const T& obj) {
std::ostringstream oss;
constexpr auto type_info = reflect(T);
oss << type_info.name() << " {\n";
for (const auto& member : type_info.members()) {
oss << " " << member.name() << ": " << member.get(obj) << ",\n";
}
oss << "}";
return oss.str();
}
Hashing a struct by iterating over its fields: template<typename T>
size_t hash_struct(const T& obj) {
size_t hash = 0;
constexpr auto type_info = reflect(T);
for (const auto& member : type_info.members()) {
hash ^= std::hash<decltype(member.get(obj))>{}(member.get(obj)) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return hash;
}
Convert between struct and tuple, tuple concatenation, named tuples: // Struct to tuple
template<typename Struct>
auto struct_to_tuple(const Struct& s) {
return std::apply([&](auto&&... members) {
return std::make_tuple(members.get(s)...);
}, reflect(Struct).members());
}
// Tuple to struct
template<typename Struct, typename Tuple>
Struct tuple_to_struct(const Tuple& t) {
Struct s;
std::apply([&](auto&&... members) {
((members.set(s, std::get<members.index()>(t))), ...);
}, reflect(Struct).members());
return s;
}
// Tuple concatenation
template<typename... Tuples>
auto tuple_concat(Tuples&&... tuples) {
return std::tuple_cat(std::forward<Tuples>(tuples)...);
}
// Named tuple
template<typename... Members>
struct NamedTuple {
REFLECT_NAMED_MEMBERS(Members...);
};Does static reflection simplify such cases? ... Outlook unclear. It's definitely gnarlier to actually write the serialize() method, and in many cases, it does feel like a better option is to write a specific domain-specific language to specify what you want to specify, with a tool to operate on it as appropriate (think something like protobufs for serialization).
Same for command line parameters. We want documentation strings, maybe dashes in the name etc.
But that can surely be solved with a little more advanced struct
Implementing serialization for complex types often requires manual code writing or external tools. With static reflection you could automate this process
template<typename T>
void serialize(const T& obj, std::ostream& os) {
for_each(reflect(T), [&](auto member) {
os << member.name() << ": " << member.get(obj) << "\n";
});
}
Simplified property systems class Person {
public:
Person(const std::string& name, int age)
: name(name), age(age) {}
std::string getName() const { return name; }
void setName(const std::string& name) { this->name = name; }
int getAge() const { return age; }
void setAge(int age) { this->age = age; }
private:
std::string name;
int age;
REFLECT_PROPERTIES(
(name, "Name of the person"),
(age, "Age of the person")
)
};
int main() {
Person person("Alice", 30);
auto properties = reflect::getProperties<Person>();
for (const auto& prop : properties) {
std::cout << "Property: " << prop.name
<< " (" << prop.description << ")" << std::endl;
auto value = reflect::get(person, prop.name);
std::cout << "Value: " << value << std::endl;
if (prop.name == "age") {
reflect::set(person, prop.name, 31);
}
}
std::cout << "Updated age: " << person.getAge() << std::endl;
return 0;
}
Simplified template metaprogramming template<typename T>
void printTypeInfo() {
constexpr auto info = reflect(T);
std::cout << "Type name: " << info.name() << "\n";
std::cout << "Member count: " << info.members().size() << "\n";
}
Easier to write generic algorithms that work with arbitrary types template<typename T>
void printAllMembers(const T& obj) {
for_each(reflect(T), [&](auto member) {
std::cout << member.name() << ": " << member.get(obj) << "\n";
});
}Imagine making a plain
struct Point { float x; float y; };
and wanting to serialize it to JSON without further ceremonyThis was a video game mod, essentially. I needed to create a text interface to modify settings for any other mod that might be installed. Other mods would simply implement a settings class with certain attributes, then I could list out all fields and their types. The list was processed into a sort of tree presented through the chat interface. From there I can generate code to modify that settings class from outside its assembly and raise value change events.
The reflection part of that was extremely simple, but just because that's how C# works. C# makes a task like this almost trivial.
At my current job, we have a similar thing. Classes decorated with attributes. We inspect them and check the generic type they implement. This way we register message handlers by their message type dynamically. You write a handler class and it simply works.
Windows Forms had a PropertyGrid control which did the same thing as my text interface, but with a grid of properties you can edit freely.
Most of this stuff is typically done at runtime. But you could have it be static if you wanted. A precious job did this to access the backing array inside of a List<> object. I offer no explanation or excuse for that one.
Because the library doesn’t need to allocate unless the underlying types being parsed to do, it has been constexpr since C++17 too.
A bunch of people have used libraries that use macros for reflection like or PFR to integrate other C++ JSON Libraries too.
They have a reference implementation on godbolt under clang, so you can play around with that. I did not try it yet.
Yes, there is magic_enum already - and we based this implementation on the core of magic_enum, but refocused for C++20, using some of the key features of this language version such constexpr algorithms, std::source_location and concepts; we also improved on and expanded the API.
Looks like it did very well in St. Louis!
I do like the examples that I see there.
This seems like the kind of language feature that I might not make much use of directly in general application code, but would wrap up in utility functions or use via lower-level libraries that the application code builds on. E.g., they showed command-line parsing, but I could also see this for benchmarking and testing frameworks to automatically find the workloads and tests in a non-hacky way.
I also wonder about how this feature interacts with translation units or modules and linkage, though. I'm reminded of the static initialization order fiasco; this seems like it might open up issues that make that look tame by comparison. (Not a complaint; I'm actually looking forward to this.)
There are still a few gotchas being ironed out, there was a talk at ACCU about many corner cases.
But the `member_number` functions in § 3.2 look disturbing to me. It's not discernible how invalid arguments are handled. Normally I'd look at generated assembly to answer a question like that, but this probably doesn't make sense with compile-time-fu (`constexpr`)…
I suppose C++'s template system might be able to generate JSON deserializers with static reflection as well
It definitely can, and it will be faster and more type-safe than doing it at runtime. But if you do want to do it at runtime, well, it's possible to implement runtime reflection as a library on top of compile-time reflection, so someone will probably do that.
And while it can currently be replaced with templates alone in fairly old versions of C++ (C++14 is the oldest I think), compile times are egregious unless you use very new, shiny features.
And as much as I am pro "move to new shiny C++", one of the big commercial uses of Qt is in semi-embedded applications like car entertainment centers where you are stuck with whatever (often outdated) toolchain your SOC source relies on. So pushing for shiny new Qt risks either splitting Qt in half or abandoning a lot of very well paying users.
I mean, GCC also has some helper tools used to compile C++ code and we don't talk about "replacing them".
Why people want to remove moc from Qt?