8
\$\begingroup\$

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>: a std::string_view of the string representation of T.
  • indi::type_name_cstr<T>: a pointer to a NUL-terminated constant char array, with the string representation of T.

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:

(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:

P2996R3Reflection 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!

\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

The implementations naturally produce a std::string_view, which is then copied into an array to append null termination (then back into a string view for type_name).

I would reorganise so that we implement indi::type_name in the two variations, each returning a string view, and use the (constexpr since C++20) std::string(std::string_view) constructor to create a null-terminated string only when needed:

template<typename T>
constexpr std::string_view type_name;  /* two implementations */


template<typename T>
constexpr auto type_name_string = std::string{type_name<T>};

template<typename T>
constexpr auto type_name_cstr = type_name_string<T>.c_str();
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Unfortunately, that requires non-transient constexpr allocation, which is a problem many C++ experts are working on right now for this exact situation: trying to get things like strings and vectors out of the compile-time reflection domain, and into the run-time. \$\endgroup\$
    – indi
    Commented Aug 23 at 19:46
  • \$\begingroup\$ It will “work” if you change the constexpr to const, but then you would be allocating all those strings at run-time, which means: 1) you pay the allocation costs for every string; 2) you give up heap space for each string for the entire run-time of the program; and 3) the string data is duplicated (in static memory for the source data, and on the heap in each string). \$\endgroup\$
    – indi
    Commented Aug 23 at 19:46
  • \$\begingroup\$ Oh, only some instantiations of std::string constructor are constexpr - shame there was no comment in the code explaining why we didn't do it this way! Still, I think that making the std::string_view version primary is an improvement even when a string isn't constexpr-constructible. \$\endgroup\$ Commented Aug 23 at 20:01
  • 1
    \$\begingroup\$ All of std::string’s constructors are constexpr; the issue is that any memory allocated at compile time must be freed at compile time. When you do constexpr auto s = std::string{"..."};, the memory is being allocated at compile time, which is fine… unless that string persists to run-time, which it must if you are going to do something like print it at run-time. \$\endgroup\$
    – indi
    Commented Aug 24 at 20:26
  • \$\begingroup\$ A string view can’t be the primary source of data because it is a view; the data it references must exist somewhere (else). And for this use case, it must exist for the lifetime of the program (so not local or temporary). If we could use the result of display_string_of(), that would be fine, because that’s promised to be static data. Since we can’t, we need to create a place for the data that will persist for the program… hence, a global variable. (Technically a global constant.) \$\endgroup\$
    – indi
    Commented Aug 24 at 20:27
3
\$\begingroup\$

std::invoke() instead of immediately-invoked lambda expressions

Both the C++26/reflection and C++23/old-school versions of the code use immediately-invoked lambda expressions, which can be tricky to spot and easy to screw up (if you forget the final parentheses, you can get some wacky errors).

An alternative design pattern I’ve explored is to never use IILEs, and instead always wrap lambdas in std::invoke(). Then it is clear what is happening, and impossible to forget to actually call the lambda (by forgetting the final parentheses).

Below is what the code would look like with std::invoke() rather than IILEs. I’ve stripped it down to only the C++26/reflection code, for brevity, but otherwise this is the whole indi/core/type_name.mpp module partition.

export module indi.core:type_name;

import std;

namespace indi {
inline namespace v1 {

template<typename T>
inline constexpr auto type_name_data =
    std::invoke([]
    {
        constexpr auto s = display_string_of(^T);

        auto data = std::array<char, s.size() + 1>{};
        std::ranges::copy(s, data.begin());
        return data;
    })
;

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

Note that type_name_data is not exported, so it is basically “private” data.

Also note the __symexport macros. They are part of the Build2 system, and may be necessary on Windows. (I don’t know for sure; I don’t use Windows.) To make the module pure standard C++ right out of the box, I would have to add #ifndef __symexport then #define __symexport to the top of this… and every module. To hell with that. If this code were ever to be compiled in a non-Build2 system, you could just add something like -D__symexport to the command line (or, more realistically, define it to whatever the compiler’s actual shared object symbol export incantation is; you’d need to do that anyway if explicit exporting is required (as it may be on Windows)).

\$\endgroup\$
1
  • \$\begingroup\$ I like the suggestion of std::invoke() here - I think I'll be using that in my own code to make the immediate evaluation more obvious. Thanks! \$\endgroup\$ Commented Aug 23 at 10:46
2
\$\begingroup\$

Reflection is not (yet) in C++26

I just want to point out that at this point in time, while there are proposals for reflection in C++, none of them have been voted on for inclusion in C++26. So your code currently needs an experimental compiler for an unofficial feature. You might as well have used the Circle C++ compiler and just used @type_string().

Some findings while reviewing this code

Code like this is just terrible to read. That's no fault of yours, it's just caused by the design of the language. At first I failed to see why you had these immediately-invoked lambdas returning std::arrays; it took me a long time to see that there is no guarantee in some cases that the reflected strings are NUL-terminated. If you would not need type_name_cstr, you could simplify the code.

It was also weird to see display_string_of(^T) without the std::meta:: prefix, but as you explained this is found via ADL. I guess it will take some getting used to these upcoming new features of C++.

\$\endgroup\$
5
  • 2
    \$\begingroup\$ “… while there are proposals for reflection in C++, none of them have been voted on for inclusion in C++26.” That is not (entirely) correct. P2996R4 was approved by the core language group at last meeting in June. The library group wanted changes to some library stuff (not the reflection design itself); R5, the version I’ve based on, has their revisions. All that’s left is: 1) the Unicode group has to review the UTF-8 stuff, and when they’re done; 2) the final standardese has to be approved. For all practical purposes, the stuff I’ve used is, as of now, C++26. \$\endgroup\$
    – indi
    Commented Aug 17 at 19:52
  • 2
    \$\begingroup\$ With display_string_of(^T) I’m using ADL. And NUL-terminated strings are absolutely required, for a number of very important use cases, like generic ostream insertion and low-level or system logging functions (syslog(), for example). I would sooner ditch the string view version… but since I basically get it for free… why not both? It’s 3 extra lines of pretty trivial code, and one extra IILE wrapping it all. I agree IILEs are tricky and hard to spot, which is why I’ve considered wrapping them in std::invoke() to make things clearer. (Or would that be less clear?) \$\endgroup\$
    – indi
    Commented Aug 17 at 19:53
  • \$\begingroup\$ I’ll add an answer with a version of the code, using std::invoke() instead of IILEs. Let’s see if that’s more readable. \$\endgroup\$
    – indi
    Commented Aug 17 at 21:06
  • \$\begingroup\$ I forgot about ADL. Although it's also weird that language features return something of a type that's already in a namespace. In those meeting notes, where exactly does it say P2996R4 was approved? \$\endgroup\$
    – G. Sliepen
    Commented Aug 18 at 9:40
  • 2
    \$\begingroup\$ Well, we’ve had sizeof returning std::size_t since the dawn of the std namespace, and of course there’s std::initializer_list, so it’s not entirely novel. As for N4985, the information is kinda scattered around and difficult to grok without understanding committee structure, so I’ll put a little extension on the review request to explain how to suss it all out, and what exactly is already in C++26. \$\endgroup\$
    – indi
    Commented Aug 18 at 16:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.