Reflection is commonplace in other contemporary programming languages, but support for it in C++ is virtually non-existent. In this article we’re going to summarize what reflection is and how existing C++ techniques to implement it could potentially be improved in the upcoming C++26 Standard.
Reflection is the ability of a running program to inspect, analyze and modify its own structure at runtime. In C++ we would prefer to focus on compile-time reflection, which would not be expected to incur runtime costs. Existing features which do incur runtime costs already in C++ include RTTI (Run-Time Type Information) and virtual function dispatch, while those which do not include TMP (Template Metaprogramming) and compile-time function invocation.
Obtaining string representations of the names of variables and objects at compile-time should not involve huge changes to compilers, or the standard library, as these are already output within error diagnostics. (A lookup table from object address to a string representation could be a compiler switch, but would require run-time support.) Calling an object based on its string representation is more involved and could involve a library type such as std::unordered_map. Below is some code to illustrate these ideas:
template<typename T>
struct NamedObject {
T value;
const char *name;
};
void f() {
auto namedInt = NamedObject{ -1, "loopCounter" };
std::cout << "Object \'" << namedInt.name
<< "\' has value: " << namedInt.value << '\n';
}
using AddressToName = std::unordered_map<void* const,std::string>;
using ObjectMap = std::unordered_map<const std::string,std::any>;
ObjectMap {
{ "loopCounter", -1 },
{ "functionName", "processData" },
// ...
};
void g(std::pair<const std::string,std::any>& namedObject) {
std::cout << "g() called with parameter \'" << namedObject.first << "\'\n";
// extract namedObject.second using std::any_cast<> and RTTI
}
In Modern C++, the C pre-processor is still considered necessary for some tasks, as discussed in a previous article. One existing use case is outputting enumeration values as string representations, as shown in the code below:
#include <print>
#define SHOW_ENUM(field) \
#define SHOW_ENUM(field) std::print("Enum field \'{}\' has value: {}\n", \
#field, std::to_underlying(field));
enum class FileAccess { NoFile = 0, Read, Write, ReadWrite, Error = 99 };
int main() {
SHOW_ENUM(FileAccess::Read);
SHOW_ENUM(FileAccess::ReadWrite);
SHOW_ENUM(FileAccess::Error);
}
The output from running this program is:
Enum field 'FileAccess::Read' has value: 1
Enum field 'FileAccess::ReadWrite' has value: 3
Enum field 'FileAccess::Error' has value: 99
It would probably not be complex to enable compiler/library support for providing enumeration fields as strings (the way the # operator works in the pre-processor is simply to put quotes around the text from the sourcecode).
In another previous article, the use of a std::unordered_map<std::string,std::function<...>> was used as a way to call a function based upon a string representation of its name. The code shown had the limitation that the return and parameter types must be the same for all lambda functions stored within the map, so its applicability may be lessened for this reason. Depending upon the use case, it might be possible to use std::variant or std::any to get around this restriction, dependent upon the application, as shown in the code below:
#include <any>
#include <variant>
#include <unordered_map>
#include <functional>
#include <string>
#include <print>
using ReturnType = std::variant<std::monostate,double,std::string>;
std::unordered_map<std::string,std::function<ReturnType(std::any,std::any)>> Functions{
{ "add", [](const auto& a, const auto& b){ return std::any_cast<double>(a) + std::any_cast<double>(b); } },
{ "negate", [](const auto& a, const auto&){ return -std::any_cast<double>(a); } },
{ "concat", [](const auto& a, const auto& b){ return std::any_cast<std::string>(a) + std::any_cast<std::string>(b); } }
};
using namespace std::literals;
int main() {
std::print("Calling \'add(1.5, 2.0)\', result: {}\n",
std::get<double>((Functions["add"])(1.5, 2.0)));
std::print("Calling \'negate(1.0)\', result: {}\n",
std::get<double>((Functions["negate"])(1.0, std::any{})));
std::print("Calling \'concat(\"Iced\", \"Bun\")\', result: {}\n",
std::get<std::string>((Functions["concat"])("Iced"s, "Bun"s)));
}
Output from running this program is:
Calling 'add(1.5, 2.0)', result: 3.5
Calling 'negate(1.0)', result: -1
Calling 'concat("Iced", "Bun")', result: IcedBun
Again, this boilerplate code could probably be provided by a library.
A complement to reflection is code synthesis at compile-time. This simplest form is a C-style macro taking a number of parameters, producing different code as these change. In C++, we usually think of generics as the way to instantiate type-specific code, but with the recent availability of constexpr std::string (and other constexpr standard library types), it could be a possibility in the near future to generate a string (at compile-time) to be turned into code (also at compile-time), which would open up many interesting possibilities for semi-automated code generation.
Looking at how the Standards Committee are thinking, proposals for reflection have involved a prefix ^ operator providing an object of std::meta::info. Unfortunately, the link for proposal paper P2996 appears to be dead at the time of writing, and there are indications that support for the proposal(s) is cooling off. For Boost’s implementation of reflection, take a look at the Boost.Describe library, which provides various methods for naming enumerations and classes/structs for C++14 (and newer) compilers.
That just about wraps things up for this article on reflection in Modern C++. Some existing ways of approximating the support found in other languages has been demonstrated, together with some indicators to ways in which the language and standard library could be improved. Realistically, it may seem to be unlikely that the proposal(s) is (are) in a suitably finished state for inclusion in C++26, which may disappoint some.