0

I want to write a generic accessor for a class member regardless whether it is a function or or a data member:

#include <type_traits>

namespace traits
{
    template <typename T, typename = void>
    struct _name;

    template <typename T>
    struct _name<T, std::void_t<decltype(std::declval<T>().name)>>
    {
        constexpr decltype(auto) operator()(const T &t) const
        {
            return t.name;
        }
    };

    template <typename T>
    struct _name<T, std::void_t<decltype(std::declval<T>().name())>>
    {
        constexpr decltype(auto) operator()(const T &t) const
        {
            return t.name();
        }
    };

    template <typename T>
    decltype(auto) name(const T &t)
    {
        return _name<T>{}(t);
    }
}

#include <string>

struct beb
{
   std::string name = "beb";
};

struct bob
{
   std::string name() const
   {
       return "bob";
   }
};

#include <iostream>

int main()
{
    std::cout << traits::name(bob());
    std::cout << traits::name(beb());
}

I am using SFINAE with void_t specialization, but it works with only single specialization. Otherwise it gives an error saying error C2953: 'traits::_name<T,void>': class template has already been defined.

MSVC latest: https://godbolt.org/z/h9WT58z8P - does not compile
GCC 12.2: https://godbolt.org/z/vc3K1M7x5 - compiles
Clang 15.0.0: https://godbolt.org/z/dqGEMfYWK - does not compile

  1. Should this code compile? (It compiles only for GCC)
  2. How to make it compilable?

_name is a customization point within a traits namespace. By default it accesses name using name or name(). But one can provide a specialization to use, for example getName().

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • 1
    What's your expectation when it's a functor - i.e., a member, that's also callable? Or a function pointer? – lorro Sep 15 '22 at 16:19
  • @lorro this is a legit but somehow rare occasion. It is not specified by design, but just for the sake of example I would say "I want it to be invoked if it can be invoked" – Sergey Kolesnik Sep 15 '22 at 16:22
  • You need two different traits, one `has_name_function_member` and another `has_name_variable_member`, Then you can use `if constexpr` to branch between the two different cases. You can't have multiple specializations that get deduced to the same types which is why this fails as is. – NathanOliver Sep 15 '22 at 16:23
  • @NathanOliver I guess that with those two traits `if constexpr` would be an overkill. Moreover, I did not tag this question as c++17 – Sergey Kolesnik Sep 15 '22 at 16:24
  • @SergeyKolesnik The C++ tag means the current standard, which is C++20. If you don't want that then you need to specify the max version. Also, `void_t` is a C++17 feature. – NathanOliver Sep 15 '22 at 16:26
  • @NathanOliver you are right, but `void_t` is a library feature, and `if constexpr` is a language feature. – Sergey Kolesnik Sep 15 '22 at 16:28
  • Then you'll need to do the branching like we did before `if constexpr`. My main point was that you can't have one trait that has three possibilities. You only get one false case (`template struct _name;`), and one true case (`template struct _name { ... };`). – NathanOliver Sep 15 '22 at 16:32
  • @NathanOliver I can't have 3 values for a boolean, that is true. But I can have as many specializations as I want. So theoretically if I make a primary template with default parameter `typeanme T, typename = get_specialization`, I can go without `if constexpr`. But I am not sure – Sergey Kolesnik Sep 15 '22 at 16:36
  • 1
    Related/possible dupe: https://stackoverflow.com/q/45948990 (Seems like the fix is `template struct non_deduced_void { using type = void; }; template using non_deduced_void_t = typename non_deduced_void::type;` or `type_identity_t>`) – Artyer Sep 15 '22 at 16:45
  • @Artyer it is a very useful answer in the context of the current question. Thank you. However I would rather have a working solution for this specific problem. – Sergey Kolesnik Sep 15 '22 at 16:48
  • 1
    `template decltype(auto) name(const T &t) { return std::invoke(&T::name, t); }` [Demo](https://godbolt.org/z/oaG4xGY86). – Igor Tandetnik Sep 16 '22 at 04:06
  • If you want to do it the hard way, then perhaps something [along these lines](https://godbolt.org/z/7rePbM97j) – Igor Tandetnik Sep 16 '22 at 04:25
  • @IgorTandetnik very nice. Can you post it as an answer? I wasn't aware of `std::invoke` being able to do these things. Very useful – Sergey Kolesnik Sep 16 '22 at 09:55

1 Answers1

1

You seem to be reinventing std::invoke. This function embodies the definition of Callable concept, and that definition has two special cases:

  • a pointer to data member is "callable" like a function taking the object as its single parameter: std::invoke(&C::dataMember, obj) is equivalent to obj.*dataMember
  • a pointer to member function is "callable" like a function taking the object as its first parameter: std::invoke(&C::memFun, obj, a, b, c) is equivalent to (obj.*memFun)(a, b, c)

Putting this together, your name can be implemented simply as

template <typename T>
decltype(auto) name(const T &t)
{
    return std::invoke(&T::name, t);
}

It will do the right thing whether name is a data member or a member function. Demo


If you want _name as a customization point, just add an extra indirection: make name call _name, and the default implementation of _name call std::invoke.

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85