This is a modernization of a very old utility I’ve had in my personal code toolbox for a while. There are two variable template constants:
indi::type_name<T>
: astd::string_view
of the string representation ofT
.indi::type_name_cstr<T>
: a pointer to aNUL
-terminated constantchar
array, with the string representation ofT
.
The utility is implemented using the C++26 reflection specification (currently P2996r5), and works on experimental forks of Clang with up-to-date reflection support. (Except that the Clang pretty-printer does not show the fully-qualified name, for now.)
If reflection isn’t supported (which, let’s be honest, it won’t be for the time being), the utility falls back on a modernized version of the old implementation, which relies on __PRETTY_FUNCTION__
macro trickery.
If you want to actually build this, you will need some up-to-date tools:
- build2 0.17.0.
- Clang 0.18.0
- You have to use
libc++
, notlibstdc++
.
- You have to use
(It should also work with MSVC 17.10 or better, but I don’t use MSVC, so I don’t know for sure.)
The code layout is:
$ tree
.
├── buildfile
├── demo.cpp
├── indi
│ ├── core
│ │ └── type_name.mpp
│ └── core.mpp
└── indi.mpp
3 directories, 5 files
$
The buildfile
is:
cxx.std = latest
cxx.features.modules = true
cxx.features.symexport = true
using cxx
cxx{*}: extension = cpp
mxx{*}: extension = mpp
./ : exe{demo} lib{indi}
lib{indi}: mxx{indi} mxx{indi/**} cxx{indi/** --indi/**.test...}
exe{demo}: cxx{demo} lib{indi}
indi.mpp
is just:
export module indi;
export import indi.core;
indi/core.mpp
is just:
export module indi.core;
export import :type_name;
And the real meat of the utility is indi/core/type_name.mpp
:
module;
#include <version>
export module indi.core:type_name;
import std;
////////////////////////////////////////////////////////////////////////
// This module provides two constexpr variable templates:
// * indi::type_name<T>
// A `std::string_view` containing a string representation of the
// type `T`.
// * indi::type_name_cstr<T>
// A pointer to a `NUL`-terminated `const` `char` array containing
// a string representation of the type `T`.
//
// There are two implementations. The first implementation uses
// reflection, as currently specified in P2996r5, and as expected to be
// standardized in C++26.
//
// The second implementation, which "works" even in C++20, uses a trick.
// Most compilers provide a macro that expands to the full function
// signature of the current function, usually called
// `__PRETTY_FUNCTION__`. By providing a function template with the type
// we're interested in as the sole template parameter, we can extract
// the string representation of that type from the macro. This is, of
// course, very compiler-dependent.
//
// There is a third implementation possibility, using
// `std::source_location::function_name()`. The big 3 compilers all
// return a flavour of `__PRETTY_FUNCTION__`, so we could make a
// portable implementation that doesn't use any macros. Except... no.
// Because some dumbass compilers, like EDG, return the same value as
// `__func__`... which is useless (nobody every uses `__func__` when
// `__PRETTY_FUNCTION__` is available), and stupid (because EDG even
// supports `__PRETTY_FUNCTION__`). Since we need to use the
// preprocessor anyway (to suss out dumbass compilers like EDG), we
// might as well just stick with the second implementation until
// reflection is a thing.
////////////////////////////////////////////////////////////////////////
#if defined(__cpp_impl_reflection) && defined(__cpp_lib_reflection)
////////////////////////////////////////////////////////////////////
// The reflection implementation.
////////////////////////////////////////////////////////////////////
namespace indi {
inline namespace v1 {
template<typename T>
inline constexpr auto type_name_data =
[]
{
constexpr auto s = display_string_of(^T);
auto data = std::array<char, s.size() + 1>{};
std::ranges::copy(s, data.begin());
return data;
}()
;
} // inline namespace v1
} // namespace indi
#else
////////////////////////////////////////////////////////////////////
// The old-school implementation.
//
// The plan is to create a type in the global namespace with a
// really ugly name, so that it can be easily and accurately picked
// out of a function template signature, then determine where in
// the compiler's pretty function signature string the type name is.
//
// So, first we create a type whose name is stored in the macro
// `INDI_PP_UGLY`, with the string version of the name in
// `INDI_PP_UGLY_NAME`. (We also keep the length of the name in
// `INDI_PP_UGLY_NAME_LEN`.)
////////////////////////////////////////////////////////////////////
#define INDI_PP_UGLY eljk0guL0ALG9w5NKotoXi7saWBL0aR8
// Helper macros to stringize the ugly identifier.
#define INDI_PP_S_(x) #x
#define INDI_PP_S(x) INDI_PP_S_(x)
#define INDI_PP_UGLY_NAME INDI_PP_S(INDI_PP_UGLY)
#define INDI_PP_UGLY_NAME_LEN std::string_view{INDI_PP_UGLY_NAME}.size()
// The ugly type:
struct INDI_PP_UGLY {};
namespace indi {
inline namespace v1 {
////////////////////////////////////////////////////////////////////
// Now we create a variable template constant that is a string view
// of the string representation of the template parameter type.
//
// Currently we only check for `__PRETTY_FUNCTION__` and
// `__FUNCSIG__` (which is the MSVC version of
// `__PRETTY_FUNCTION__`). If we wanted to add support for other
// possibilities, we could add it here.
////////////////////////////////////////////////////////////////////
template<typename T>
consteval auto _pretty_func_impl_()
{
#if defined(__GNUC__)
return std::string_view{__PRETTY_FUNCTION__};
#elif defined(__FUNCSIG__)
return std::string_view{__FUNCSIG__};
#else
#error Unsupported compiler
#endif
}
template<typename T>
inline constexpr auto _pretty_func_ = _pretty_func_impl_<T>();
////////////////////////////////////////////////////////////////////
// Now we find the ugly type name in the
// `_pretty_func_<INDI_PP_UGLY>` string, and determine where in the
// string it appears. Once we know how long the part before is (the
// prefix), and how long the part after is (the suffix), we can (in
// theory) just lop off those for any `_pretty_func_<T>` string, and
// be left with the string representation of `T`.
//
// We do *LOTS* of static asserting to make sure our assumptions
// hold.
////////////////////////////////////////////////////////////////////
static_assert(
_pretty_func_<::INDI_PP_UGLY>.contains(INDI_PP_UGLY_NAME),
"_pretty_func_<T> must contain T"
);
static_assert(
not _pretty_func_<::INDI_PP_UGLY>
.substr(
_pretty_func_<::INDI_PP_UGLY>.find(INDI_PP_UGLY_NAME)
+ INDI_PP_UGLY_NAME_LEN)
.contains(INDI_PP_UGLY_NAME),
"_pretty_func_<T> must contain T only once"
);
inline constexpr auto _pretty_func_prefix_size_ =
_pretty_func_<::INDI_PP_UGLY>.find(INDI_PP_UGLY_NAME)
;
static_assert(_pretty_func_prefix_size_ != std::string_view::npos);
static_assert(
_pretty_func_<::INDI_PP_UGLY>
.substr(_pretty_func_prefix_size_)
.starts_with(INDI_PP_UGLY_NAME)
);
inline constexpr auto _pretty_func_suffix_size_ =
_pretty_func_<::INDI_PP_UGLY>.size()
- (_pretty_func_prefix_size_ + INDI_PP_UGLY_NAME_LEN)
;
static_assert(
_pretty_func_<::INDI_PP_UGLY>.size()
==
(
_pretty_func_prefix_size_
+ INDI_PP_UGLY_NAME_LEN
+ _pretty_func_suffix_size_
)
);
////////////////////////////////////////////////////////////////////
// Now we can finally determine the type name by extracting it from
// the full pretty function signature string.
////////////////////////////////////////////////////////////////////
template<typename T>
inline constexpr auto type_name_data =
[]
{
constexpr auto full_pretty_func_sig = _pretty_func_<T>;
static_assert(
full_pretty_func_sig.size()
> _pretty_func_prefix_size_ + _pretty_func_suffix_size_
);
// Remove the known prefix and suffix.
constexpr auto s = full_pretty_func_sig.substr(
_pretty_func_prefix_size_,
(full_pretty_func_sig.size()
- (_pretty_func_prefix_size_ + _pretty_func_suffix_size_))
);
static_assert(not s.empty());
auto data = std::array<char, s.size() + 1>{};
std::ranges::copy(s, data.begin());
return data;
}()
;
} // inline namespace v1
} // namespace indi
#endif // defined(__cpp_impl_reflection) && defined(__cpp_lib_reflection)
namespace indi {
inline namespace v1 {
export __symexport
template<typename T>
inline constexpr auto type_name_cstr = type_name_data<T>.data();
export __symexport
template<typename T>
inline constexpr auto type_name = std::string_view{type_name_data<T>.data()};
} // inline namespace v1
} // namespace indi
I have also included a simple demo program in demo.cpp
:
// A simple demo program, just to show the idea works.
//
// A full test suite would probably require using regular expressions
// to check the expected type name strings. But the only regex library
// I know is the standard one, and that's just a mess.
//
// Honestly, I haven't bothered with a proper test suite because I've
// only ever used this stuff for debugging and testing, never in real
// code. If people are interested in using this in real code, I'll
// write proper tests.
import std;
import indi;
namespace foo {
template <typename T, int I>
struct bar {};
} // namespace foo
auto main() -> int
{
std::println("indi::type_name<int> = \"{}\"", indi::type_name<int>);
std::println("indi::type_name<foo::bar<double, 2>> = \"{}\"", indi::type_name<foo::bar<double, 2>>);
std::println("indi::type_name<int const&> = \"{}\"", indi::type_name<int const&>);
std::println("indi::type_name<std::string&&> = \"{}\"", indi::type_name<std::string&&>);
}
To build and run the demo:
$ b config.cxx=clang++ config.cxx.coptions=-stdlib=libc++
c++ /usr/local/share/build2/libbuild2/cc/mxx{std} -> build/cc/build/modules/cxx/bmis{std}
c++ indi/core/mxx{type_name} -> indi/core/bmia{type_name}
c++ indi/core/mxx{type_name} -> indi/core/bmis{type_name}
c++ indi/mxx{core} -> indi/bmia{core}
c++ indi/mxx{core} -> indi/bmis{core}
c++ mxx{indi} -> bmia{indi}
c++ mxx{indi} -> bmis{indi}
c++ cxx{demo} -> obje{demo}
ld libs{indi}
ar liba{indi}
ld exe{demo}
$ ./demo
indi::type_name<int> = "int"
indi::type_name<foo::bar<double, 2>> = "foo::bar<double, 2>"
indi::type_name<int const&> = "const int &"
indi::type_name<std::string&&> = "std::__1::basic_string<char> &&"
$
I am slowly modernizing my personal code toolbox toward a modules-based system. This utility, once it passes review, will be one of the parts of my new personal code library.
I am aiming for a very modern library, right on the cutting edge of current technology, so that it will be useful for many years to come. So I would like the code to be reviewed as a next-generation C++ utility more so than a current generation tool. I am particularly interested in ideas about how to make this utility as future-proof as possible. I would also like to hear opinions about the code structure and the use of modules, sub-modules, and module partitions to organize. And of course, I would love suggestions about other features or design patterns that might work, or any other ideas, improvements, or whatever else you may have in mind.
Thanks in advance!
C++26 and reflection
C++26 is currently in development, but there have already been 4 full standard committee meetings dedicated to it (not to mention a bunch of telecons), the first in Varna in June 2023, and the most recent in St. Louis in June 2024.
The 3 “big” features targeted at C++26 are:
- Reflection
- Contracts
- Senders/receivers (
std::execution
)
(Personally, I would add the linear algebra library—which is already in since the second meeting—and pattern matching.)
Contracts is still under heavy development (there seems to be a lot of kerfuffle about UB and side-effects in contracts, but I think the syntax is already approved; honestly, not really been following that too closely recently). std::execution
is mostly accepted, but seems to be really contentious, so… we’ll see.
So what about reflection?
The reflection proposal is huge, consisting of both language extensions and library facilities, so it was never going to be accepted all in one go. But as of the St. Louis meeting, the language stuff has been approved.
To see the committee progress, you can look at the minutes of the St. Louis meeting (N4985). The “standard committee” is actually really a bunch of sub-committees—that’s where all the actual work gets done—that work in stages. The first stage is the design stage; that’s where all the tinkering happens. SG-7 was the design group that primarily handled reflection. The second stage is where the designs get prepared for actual standard inclusion. And the third stage is after they have been accepted, where the actual standardese wording gets approved. Basically, the study groups at stage 1 design a feature; then the evolution groups at stage 2 standardize it; then the core groups at stage 3 finalize the wording; then the full plenary locks it in as “the standard”.
So to see what’s going to be in the next standard the groups you want to look for are:
- CWG: this is the stage 3 group that finalizes the standard wording for core language features. If you want/need the actual standardese for a language feature, this is the group that has the final say on that.
- LWG: this is the stage 3 group that finalizes the standard wording for library features. If you want/need the actual standardese for a library component, this is the group.
- EWG: this is the stage 2 group that finalizes the design for core language features. If want to see what is ”in” the standard, but don’t care about the precise standardese wording, this is the group to watch.
- LEWG: this is the stage 2 group that finalizes the design for library components. If want to see what is ”in” the standard library, but don’t care about the standardese, this is the group.
I don’t really care about the standardese; I care more about the design of reflection, so I am watching EWG for the core features, and LEWG for the library stuff.
So in N4985, what you want to do is skip down to the “review of the meeting” section (section 7, starting on page 5), then look for ”Evolution” (EWG) on page 10 for the language features, and “Library Evolution” (LEWG) on page 12 for the library stuff.
The EWG section just says:
● P2996R3 — Reflection for C++26: moving towards C++26.
In other words: it’s in. Reflection, as describe in revision 3 of the proposal paper has been language design approved, and passed on to CWG for wording review. (And CWG has already started that, but more on that in a moment.)
The LEWG section is actually clearer:
“P2996R4: Reflection for C++26” is under review on LEWG. It provides the
std::meta
namespace, which contains library functions to support “reflection” functionality, such as traits-equivalent functions and query functions, as well as functions to construct structures based on information from reflected code.EWG (the language evolution group) approved the language aspect of the proposal, and LEWG (the standard library evolution group) is in the work of reviewing the library aspects of it.
There’s a little more detail further down where the group notes P2996 as a paper it has to see again.
So what actually happened is the LEWG people actually reviewed revision 3… then made some changes and “officially” presented revision 4 to the group. That was not approved either, so more changes were made after, giving us the current revision 5.
But, helpfully, the LEWG minutes very clearly state that the language stuff is in.
If you want even more detail about the review process, you can check the GitHub issues list. There is a comment by JF Bastien for the EWG review back in June, where you can see it passed on CWG. And a day or two later, you can see that CWG has already started the wording review.
You can also see the LEWG review status there, too. A ton of the library functions have already been approved, mostly query functions.
display_string_of()
has not been approved yet. Revision 5 has been at least partially reviewed as of just a couple days ago, but not that part. I don’t know if they are considering another change to the “name-of” stuff, or if they just haven’t got around to reviewing the last set of changes. But, I mean, whatever form it takes, there will be some kind of “name-of” function. Worst case scenario for me is I’ll have to change the name of the function I call. (Which I’ve already had to do!)
Note that while P2996 is “the reflection proposal”, it is actually only one of a number of proposals that will, altogether, make up “reflection” in C++26. If you take a look at the SG-7 stuff in N4985, six other papers were reviewed and forwarded to either EWG or LEWG (including my favourite: code injection with token sequences). It’s pretty much guaranteed that most, if not all of those, will be approved at the next meeting in Wrocław in November. And there’s still a lot of time until the C++26 feature freeze, which should happen in February 2025 in Hagenberg.
So: can you start writing, or at least planning, reflection code now? Yes… sorta. I would say it is “safe” to write reflection code that just queries… like, for example, just getting the type display string, as I’m doing. It is not yet “safe” to write generative reflection code… like, for example, an enum-to-string function. (I mean, you can “fudge” it, but it will be ugly.) But just give it a couple months!