Search code examples
c++reflectionc++20std-source-location

How to get data member names with source_location::function_name


After reading this source (and also my answer) and this source, I got the impression that we can use std::source_location::function_name to extract names of data members.

Let's say we are given some struct_t type with an instance of my_struct. Further, the following properties are valid for struct_t:

  1. The number of non-static data members is 3;
  2. The following line compiles successfully and has the expected behavior:auto& [a,b,c] = my_struct;;
  3. Any additional assumptions if this is really necessary

Q: How to use C++20 without other libs and std::source_location::function_name to extract all non-static data member names from struct_t? The correct answer should be easily generalized to an arbitrary number of fields and also be compatible with g++, clang, and MSVC.

The code below shows that this task is quite possible. But first, it requires a copy constructor and is also not compatible with MSVC, which means it violates the C++20 language standard. Your answer may use this code as a starting point or not use it at all

#include <iostream>
#include <type_traits>
#include <source_location>
#include <vector>
#include <string_view>
#include <array>    

struct struct_t {
    int field1;
    double field2;
    std::vector<int> field3;
};

template<auto p>
std::string_view get(){
      return std::source_location::current().function_name();
}

template<class T>
auto fields3(T&& st){
    static auto inst{std::forward<T>(st)};//bad. Needs a copy or move constructor 
    auto& [a,b,c]=inst;
    return std::array{get<&a>(), get<&b>(), get<&c>()};
}

int main()
{
    for (auto field:fields3(struct_t{})){
        std::cout<<field<<"\n";
    }
    //std::string_view get() [p = &inst.field1]
    //std::string_view get() [p = &inst.field2]
    //std::string_view get() [p = &inst.field3]
    return 0;
}

Solution

  • Here is the approach the linked library seems to take:

    template <typename T>
    struct Wrapper
    {
        const T value;
        static const Wrapper<T> fake;
    };
    
    template <typename T>
    consteval const T& get_fake()
    {
        return Wrapper<T>::fake.value;
    }
    
    struct Tester
    {
        Tester() = delete;
        Tester(const Tester&) = delete;
    };
    
    struct Foo
    {
        Tester test;
    };
    
    template <auto... Args>
    consteval std::string_view get_name()
    {
        return std::source_location::current().function_name();
    }
    
    template <typename T>
    consteval auto members_tuple()
    {
        const auto& [a] = get_fake<T>();
        return std::tie(a);
    }
    
    template <typename T>
    consteval std::string_view get_name_binding()
    {
        constexpr auto members = members_tuple<T>();
        return get_name<&std::get<0>(members)>();
    }
    
    int main()
    {
        std::cout << get_name_binding<Foo>();
    }
    

    Demo

    The basic strategy seems to be to pass pointers to fields of a static member object that's declared but never defined. As such, the object is never actually constructed. Accessing the fields of such an object is OK in this very specific circumstance though, so yes, this is standard-compiliant as far as I can tell.

    Is it portable though? No. This relies on parsing the string returned by std::source_location::current().function_name(), which is implementation-defined and may or may not actually contain the names of the fields pointed to by the pointers passed to it. On all three major compilers it does (though MSVC is a bit picky and sometimes doesn't depending on the exact context), but that isn't required.