Reflection in C++14: Explanations
blog.simon-ninon.fr
blog.simon-ninon.fr
...in a web framework in C++, where presumably the developer would need to write C++ anyway to make use of it?
This article is quite long and complex
No kidding. And to me, all that complexity just screams "you're doing it wrong."
I read this article all the way to the end, and was disappointed to find that, after all that code, it ends with a single macro call --- in C++ --- and leaves the reader wondering about the original goal of using JSON.
Years ago I spent a brief amount of time maintaining Java code that was written in this reflection-heavy, everything-configurable (XML and even some ad-hoc text formats, but similar idea) style, and it was not at all enjoyable to get everything set up correctly nor debug the monstrosity. On a somewhat related note, http://discuss.joelonsoftware.com/default.asp?joel.3.219431....
I highly doubt that this is making author's developers life easier.
More likely, it's just an over-engineering indulgence, that happens to make one's job more secure.
This is essentially a rather complicated way to build an interpreter for another language. To display web pages. Sort of like PHP.
This might be worth the trouble if you were using it to allow programmable shaders in a renderer. Those are usually composed of functional blocks strung together, they have to go very fast, and they're composed by people who are artists, not programmers, often through some GUI. There, it might not be overkill.
The routing configuration through a JSON configuration file was an idea I had at the beginning and it lead me to reflexion. I found the subject quite challenging and interesting, and I decided to work on it, resulting in a reflexion library.
However, when I came back to the original library, the mvc framework, I did not decide to go further concerning this routing configuration for multiple reasons: 1. the additional cost for each route call, resulting in decreased performance. 2. even if configuration would have been easier with JSON (or whatever) configuration file, the developer would have to register his controllers and actions for reflexion: lots of complexity for no real gain, as you have mentioned. And of course, if we want to avoid registering step, we would fall back in the dlopen solution as explained in the article, not really more appreciable.
I will maybe do an update to clarify this because lots of people seem to give a real importance to this part while it was only a way for me to introduce the subject. The goal of this article was mainly to describe briefly possible implementations of reflexion in C++, especially the one I have worked on.
How to kill your time by making simple things complex, then how to kill more time by maintaining it, then how to kill more and more time for adding one small feature.
Otherwise some readers will end up frustrated with the solution after absorbing tons of detail, and others will have given up even though they might have liked the result.
There are plenty of simpler things I'd like them to tackle before. Just in the past week of work two examples come to mind (just to illustrate that there are still issues)
- I was trying to get some function calls to inline and had to fight the compiler for half the day to get it to inline them for me (I still don;t understand why it's okay that the compiler ignored the inline keyword. Can't we assume that I as the developer will know what's best if I explicitly put the keyword there?)
- Then the next day I had the opposite problem where a branch was being inlined even though I wanted a if(/rare event/){ jump to some method}. Again the language provides no tools for me to accomplish that
There are tons of issues with the standard library containers and data locality that have been ignored too.
C++ is THE high performance language - can we spend some time on performance and stop pretending that there isn't room for improvement on that front?
This seems like the kind of yak shaving that can really ruin a perfectly good project.
That isn't to say configuration doesn't have its place! One of the reasons I love python and interpreted languages is it is very easy to do when you need it.
Anyway, here is an extreme oversimplified introspection engine I just wrote. It only implement creating wrapper "generic" object by class name. While we both used some of the same tricks, I have a few more that make introspection simpler. My implementation also work on embedded systems without a working "dynamic_cast" and C++ runtime type identifier.
https://gist.github.com/Elv13/715fb1b3dc4b774a06ca
Edit: Just to clarify, this is a much smaller subset of what you implemented in the blog post, but it can be extended into a full introspection system using more lambdas maps and variadic template tricks.
https://gist.github.com/Elv13/fa854e502c6c076f6bb8
REGISTER_METHOD(MyClass, myMethod3) NO_ARG
REGISTER_METHOD(MyClass, myMethod ) ARG(std::string) ARG(std::string) NO_ARG
REGISTER_METHOD(MyClass, myMethod2) RET(int) NO_ARG
Without boost preprocessor!
/me ok, time to stop wasting time on this. Please, myself, forget about this and go to sleep
See Rust's serde for an example of that for serialization.
There are a few use cases which aren't very obvious like having a vector for each element in a structure instead of having a vector of the structure. This greatly improves cache locality and can be a great gain in performance for some applications.
Of course this is more introspection which is a specific case of reflection but all these examples are definitely not 'ugly hacks' and could greatly improve C++ as a language.
The plugin registration mechanism involves a pre processor macro that authors invoke on each plugin class after definition -- this macro declares that the specified plugin implements a special factoryinstance template class. The factoryinstance template class is paramterized by the plugin type and the specific plugin implementation and provides a default constructor that retrieves the class name of the plugin implementation class via the qt meta object system and adds it to a central registry for each plugin type. This template class also contains a static member variable of the self same factoryinstance template class. This member variable gets initialized by default by the compiler during static initialization which causes the default constructor for the specialized type to be called which causes the plugin to be added to a registry with correct name and correct type information intact ...
This was all well and good however had a very annoying consequence for me as i wanted to embed some of the functionality of this system into a reusable static archive (the original system authors were only concerned with shared library scenarios). At the end of the day no code linking against this archive will make any reference to any symbols of any of these template class implementations -- so the linker will strip all these symbols away and remove all the static initialization calls -- leaving an empty plugin registry in the runtime environment of The final binary ...
For now I work around the issue by requiring that each consumer of the static library pass linker flags to force load the whole static -- this works however makes the binaries larger than needed as each binary must include all plugins and the linker is not able to include just the ones used ... At some point I'm going to need to figure out the smallest set of changes to this registration mechanism to adapt this library to the embedded use case such that a consumer of the library will include only the plugins they use ... I haven't figured out what that looks like yet ...
This issue was kind of a mind warping ... Sometimes the more I learn about C++, the more insane it seems ...
Or, if not, other languages will look at C++ to see how they handle these features and keep performance, and improve their compilers.
I've noticed this even between C++ and C - without classes, inheritance, and so forth, you tend to think a lot more about whether you need something class-like before going ahead and doing it, and many times a simple function is all you need.
Adding new features almost certainly invites plenty of advocates who will opine about how much better they are (which is true in some situations), encourating their use, even if not actually necessary. This is problematic when these features make it easy to generate large amounts of inefficient code.
Compile time has gotten worse though with some new features. Hopefully modules will help cut it back a bit.
For instance, C programmers will think a lot more about the size of buffers, how big they need to be, can they be fixed size, can they be reused, etc, because expanding them is often a manual task. In C++ it's so easy to do things like this even though it could be done much more efficiently:
string parse(const string& data) {
stringstream result;
for (char c : data) {
if (someCondition(c)) {
result << transform(c);
}
}
return result.str();
}Im not sure I get your example. Are you implying that this C++ code is inefficient because the allocation for result is not done in advance? If you know the size of result in advance, there is no reason to use a stream for result. You can simply use std::string and reserve.
Oh yes there is a reason. The reason is that streams are a convenient abstraction because there is very likely an operator<< for whatever transform(c) returns. And why is there an operator<<? Because streams abstract from the type of sink (string, file, network).
You are absolutely right, that this can be implemented a lot more efficiently. That's exactly my point. Abstractions lure us into using inefficient solutions without thinking.
You say "If you know the size of result in advance [...]". Well, exactly. Do you? That's something C programmers think about long and hard but you don't have to think about it if you use streams or even string abstractions naively.
This isn't just a case of "you can write bad code in any language". It's what abstractions do. Allow us to ignore stuff that is unnecessary if the goal is simply to arrive at a correct but less efficient solution.