top | item 40836594

Reflection for C++26

196 points| svlasov | 1 year ago |isocpp.org

208 comments

order

lallysingh|1 year ago

Wow this got really long. I was one of the coauthors for a reflection proposal (N3340) over a dozen years ago. Implementing compile-time reflection is honestly trivial - you basically transfer data from the symbol table on-demand into template specializations. It was roughly 1500 LOC to modify g++ to do it.

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.

cb321|1 year ago

I think another good example might be extracting function signatures (parameter names & types, default values -- if any) at compile-time for various purposes.

stiglitz|1 year ago

By ”stagnation” do you mean “not getting new features”?

qalmakka|1 year ago

While I love this paper and this proposal in general, as a C++ developer every time C++ adds a new major feature I get somewhat worried about two things:

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`?).

naertcxx|1 year ago

I think the focus on smart pointers is a huge mistake. Code bases using shared_ptr inevitably will have cycles and memory leaks, and no one understands the graphs any more.

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.

pjmlp|1 year ago

While I share the feeling, I don't feel that my daily languages (Java, C#, TypeScript) are getting that far behind.

Even Go is rediscovering that staying simple just doesn't happen for any language that gets industry adoption at scale.

fragmede|1 year ago

Yeah that's because you're not supposed to be using "new" anymore since the introduction of smart pointers in C++11. Std::shared_ptr and std::unique_ptr are preferred. Shared pointers ref count and auto-delete, and unique pointers can't be copied.

oldpersonintx|1 year ago

There is no alternative to modernizing C and C++

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

w4rh4wk5|1 year ago

Do I understand correctly that this proposal does not include annotations (i.e. attributes).

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

stefanos82|1 year ago

Can I ask a naive question that consists of two parts and please don't flame me? lol

  * 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?

edflsafoiewq|1 year ago

Here are some examples from the linked paper

* 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

jcranmer|1 year ago

The main canonical use case for static reflection is serialization, where serialize<T>() can easily have a default case of calling serialize() on each of the fields. In the more general case, you basically want to have some library method that does something based on the structure of a struct or class, without having to define some sort of explicit, intrusive interface that said struct or class implementation has to provide.

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

greenavocado|1 year ago

Any sort of reflection brings C++ one step closer to Python.

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

quotemstr|1 year ago

> What type of problems static reflection could solve, in general?

Imagine making a plain

    struct Point { float x; float y; };
and wanting to serialize it to JSON without further ceremony

utensil4778|1 year ago

Maybe this doesn't count as static, but I used to regularly use reflection in C# to generate code for interacting with foreign DLLs.

This 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.

bdd8f1df777b|1 year ago

What I commonly need is JSON serialization/parsing directly with structs.

gpderetta|1 year ago

I have been waiting for static reflection for the last 20 years. The current proposal seems quite nice, but the real question is whether any non trivial usage will kill compilation performance.

a1o|1 year ago

The implementation that exists for clang is fast but we will see how it goes with MSVC and GCC.

TillE|1 year ago

Finally. I think there have been proposals since C++17 at least, and all I really wanted is for them to solve the common problem of basic static reflection for enums (without hacks like magic_enum uses).

jjmarr|1 year ago

magic_enum is killing my build time with endless template instantiations. Is this going to be faster?

ahartmetz|1 year ago

This looks surprisingly fine! The opaque, extensible types remind me of Win32 with its extensibility through ever new message types. The syntax looks better than expected, too - well, it's better than templates...

pjmlp|1 year ago

Note that there are links pointing to examples on Compiler Explorer, using the EDG and clang preview implementations.

a_e_k|1 year ago

Yes, that's a clever way of demonstrating viability (and with more than one compiler implementation).

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

guardian5x|1 year ago

With every new C++ feature, I can't help to think "Oh yea, cause C++ isn't complicated enough"

tempodox|1 year ago

Highly interesting, I'm looking forward to this.

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`)…

flykespice|1 year ago

Has finally the committee come to reflection after decades of standard revisions and footguns? /j

Dwedit|1 year ago

Compile time or runtime? Compile time reflection would be completely painless and bloat-free.

thechao|1 year ago

Having implemented reflection in languages like C(++), before, it is most certainly not bloat-free. There are sorts of 'obvious' things programmers do (like enum-reflection) that end up injecting strings all over the place. The overhead is (worst case) proportional to the source-code size, in those cases. In other cases, you end up with bloat proportional to heavily-utilized template libraries. However, unless the reflection system is very clever, i.e., exposes enough information to the linker to strip duplicate-ish symbols, you end up with a bunch of reflection copies.

shortrounddev2|1 year ago

RTTI is a super vital feature for deserializing without having to generate code or write a ton of tedious boilerplate. I'd be very happy with RTTI in C++ and my life would be instantly easier if there were RTTI in typescript so I didn't have to use any of the hacky solutions out there for deserializing JSON on backends without RTTI.

I suppose C++'s template system might be able to generate JSON deserializers with static reflection as well

kstrauser|1 year ago

I haven't touched C++ since undergrad. Neither have I written any Qt code. But from memory, doesn't Qt's moc implement some of this stuff because it wasn't available in C++? Could this replace moc?

jacoblambda|1 year ago

Qt's moc can already be replaced and it increasingly is being relied on less and less as time goes on but dropping moc requires dropping all lower C++ standards or maintaining separate moc and modern-c++ versions.

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.

fluoridation|1 year ago

Qt has straight-up dynamic reflection. You can get pointers to functions from strings, and such. This is just static reflections (which is still very useful!), so it's not a complete replacement. Even if it was, I would Qt would replace its build system.

self_awareness|1 year ago

I've always wondered what's the point of "replacing moc". I mean what's the problem with moc? It's required by Qt, and completely transparent by the build system. You don't even know it's used.

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?

account42|1 year ago

I'm not convinced that wasting a simple clean syntax (prefix unary ^) is warranted for something that should be rare outside of a few libraries.

forrestthewoods|1 year ago

I'm surprised at the positive response in this thread. I find the syntax of this beyond atrocious! My goodness C++ really does not know how to do anything simply does it?

z_open|1 year ago

Many C++ features are useless outside of writing libraries, but your typical developer is going to be forced to understand how they work at some point. The result is just a burden.

pjmlp|1 year ago

Spend some time with languages like Haskell, Rust and Perl, and it will grow on oneself.

huhtenberg|1 year ago

Oi vey. Poor C++. Look how they massacred my boy.

raymond_goo|1 year ago

Ctrl-F "networking", cry, close page...

See also: https://github.com/cplusplus/networking-ts

account42|1 year ago

What does this have to do with reflection. Also why do you need networking in the C++ standard library? Networking is neither something that is relevant as a vocabulary that needs to be common between libraries nor is it something that makes sense to be set in stone like basic algorithms. Just use OS interfaces or a third-party abstraction FFS.

lpribis|1 year ago

What? This is only the proposal for reflection. Networking is completely separate.

SilverSlash|1 year ago

How about: Deprecation for C++26?