4

This question continues Non-static data members class deduction

Here are the unnamed argument functions, that I'm using to return std::string representation of the data type

struct Boo {};
struct Foo {};

std::string class2str(const double) { return "Floating"; };
std::string class2str(const int) { return "Fixed Point"; };
std::string class2str(const Foo) { return "Class Foo"; };
std::string class2str(const Boo) { return "Class Boo"; };

int main(int argc, char* argv[]) 
{
    int    x_a;
    double x_b;
    Foo    F;
    Boo    B;
    std::cout << "x_a     :" << class2str(x_a) << std::endl;
    std::cout << "x_b     :" << class2str(x_b) << std::endl;
    std::cout << "Foo     :" << class2str(F) << std::endl;
    std::cout << "Boo     :" << class2str(B) << std::endl;
};

For type deduction from the non-static member, I am using template:

struct Foo { double A = 33; }

template<typename Class, typename MemType>
std::string class2str(MemType Class::* mData)
{
    return class2str(MemType{}); // Use of empty constructor
}

std::cout << "Foo::A  :" << class2str(&Foo::A) << std::endl;

But this template requires the creation of an object with an empty constructor, which may simply not be there

struct Boo 
{
    double A;
    Boo() = delete;
    Boo(int x) :A(x) {};
};

struct Foo 
{
    double A = 33;
    Boo    b{ 0 };
};

// Compilation error: use of deleted function ‘Boo::Boo()’
std::cout << "Boo::b  :" << class2str(&Foo::b) << std::endl;

How to implement this functionality, but without calling an empty constructor?

See online demo: https://onlinegdb.com/lpc5o8pUKy

JeJo
  • 30,635
  • 6
  • 49
  • 88

6 Answers6

5

(As I started writing the answer there was no answer to the question, but as I was about to post it I saw @Jarod42's answer which already show the tag dispatch approach. Posting this answer nonetheless as it uses a slightly different approach of full specializations of a deleted primary template, instead of non-template overloads)


You can use tag dispatch to delegate calls:

#include <iostream>

struct Boo {
  double A;
  Boo() = delete;
  Boo(int x) : A(x){};
};

struct Foo {
  double A = 33;
  Boo b{0};
};

namespace detail {
template <typename T> struct Tag {};

template <typename T> std::string class2str_impl(Tag<T>) = delete;
template <> std::string class2str_impl(Tag<double>) { return "Floating"; };
template <> std::string class2str_impl(Tag<int>) { return "Fixed Point"; };
template <> std::string class2str_impl(Tag<Foo>) { return "Class Foo"; };
template <> std::string class2str_impl(Tag<Boo>) { return "Class Boo"; };

} // namespace detail

template <typename T> std::string class2str(T) {
  return class2str_impl(detail::Tag<T>{});
}

template <typename Class, typename MemType>
std::string class2str(MemType Class::*) {
  return class2str_impl(detail::Tag<MemType>{});
}

int main() {
  int x_a{42};
  double x_b{4.2};
  Foo F{};
  Boo B{x_a};

  std::cout << "x_a     :" << class2str(x_a) << std::endl;
  std::cout << "x_b     :" << class2str(x_b) << std::endl;
  std::cout << "Foo     :" << class2str(F) << std::endl;
  std::cout << "Boo     :" << class2str(B) << std::endl;
  std::cout << "Boo::b  :" << class2str(&Foo::b) << std::endl;
};

where the primary template of class2str_impl may either be deleted (as above), or implement a custom message that a given type does not have a mapped string.

dfrib
  • 70,367
  • 12
  • 127
  • 192
3

All your overloads currently take object. You might take type instead, or object which hold type:

template <typename T> struct Tag{};

std::string class2str(Tag<double>){ return "Floating";};
std::string class2str(Tag<int>){ return "Fixed Point";};
std::string class2str(Tag<Foo>){ return "Class Foo";};
std::string class2str(Tag<Boo>){ return "Class Boo";};


template<typename Class, typename MemType>
std::string class2str(Tag<MemType Class::*>)
{
    return class2str(Tag<MemType> {});
}

With usage:

int main(int argc, char *argv[]) {
    int    x_a;
    double x_b;
    Foo    F;
    Boo    B;

    std::cout<< "x_a     :" << class2str(Tag<decltype(x_a)>{}) <<std::endl;
    std::cout<< "x_b     :" << class2str(Tag<decltype(x_b)>{}) <<std::endl;
    std::cout<< "Foo     :" << class2str(Tag<decltype(F)>{}) <<std::endl;
    std::cout<< "Boo     :" << class2str(Tag<decltype(B)>{}) <<std::endl;
    // or
    std::cout<< "int     :" << class2str(Tag<int>{}) <<std::endl;
};
Jarod42
  • 203,559
  • 14
  • 181
  • 302
3

Different from the tag dispatch and the specialization techniques talked in the other answers, here is a different approach using 's constexpr if.

  • First, we find the type of the member from the member pointer using a trait (mem_type)

  • Secondly, we write an internal helper function (i.e. helper::class2str()), which uses the compiled time type checking and discard the false branch(i.e. if constexpr), so that we correctly return the data type representation as const char* literals (because we can make the function constexpr)!

  • Lastly, we will have the main class2str() which actually check the template argument type is a member pointer or not, and do branches as per again using if constexpr. If the template type is a member pointer, we get the member type using the trait mem_type and pass it to the helper::class2str().

#include <type_traits>  // std::is_same_v, std::is_member_pointer_v

// trait to get the member type
template<typename Class> struct mem_type {};
template<typename MemType, typename Class> struct mem_type<MemType Class::*> {
    using type = MemType;
};
// alias for mem_type<T>
template<typename Type> using mem_type_t = typename mem_type<Type>::type;

namespace helper
{
    template<typename Type> constexpr auto class2str() noexcept
    {
        if constexpr (std::is_same_v<Type, int>)           return "Fixed Point";
        else if constexpr (std::is_same_v<Type, double>)   return "Floating";
        else if constexpr (std::is_same_v<Type, Boo>)      return "Class Boo";
        else if constexpr (std::is_same_v<Type, Foo>)      return "Class Foo";
    }
}

template<typename Type>
std::string class2str()
{
    if constexpr (std::is_member_pointer_v<Type>) 
        return helper::class2str<mem_type_t<Type>>();
    else 
        return helper::class2str<Type>(); 
}

Now you can use it like:

std::cout << "x_a     :" << class2str<int>() << '\n';
std::cout << "x_b     :" << class2str<double>() << '\n';
std::cout << "Boo::b  :" << class2str<decltype(&Boo::A)>() << '\n';
std::cout << "Foo::b  :" << class2str<decltype(&Foo::b)>() << '\n';

Here is (the complete demo)

JeJo
  • 30,635
  • 6
  • 49
  • 88
0

Make a class template and specialzie it for various types:

template <typename T> struct TypeNameHelper {};
template <> struct TypeNameHelper<int> { static constexpr std::string_view name = "Integer"; };
template <> struct TypeNameHelper<float> { static constexpr std::string_view name = "Real"; };

I would also add a variable template for a shorter syntax and to preprocess the type if needed:

template <typename T>
inline constexpr std::string_view TypeName = TypeNameHelper<std::remove_cvref_t<T>>::name;

Then you can do:

std::cout << TypeName<int> << '\n';
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
0

One idea could be to use the function arguments to call function overloads that only requires a template parameter:

struct Boo {
    double A;
    Boo()= delete;
    Boo(int x) :A(x){};
};

struct Foo {
    double A = 33;
    Boo    b{0};
};
#include <type_traits>
#include <utility>

template<class T> std::string class2str(); // primary
// specializations
template<> std::string class2str<double>(){ return "Floating"; };
template<> std::string class2str<int>(){ return "Fixed Point"; };
template<> std::string class2str<Foo>(){ return "Class Foo"; };
template<> std::string class2str<Boo>(){ return "Class Boo"; };

// taking by argument
template<class T>
std::string class2str(const T&) { 
    return class2str<std::remove_cv_t<std::remove_reference_t<T>>>();
}

// class member by argument
template<typename Class, typename MemType>
std::string class2str(MemType Class::*) {
    return class2str<std::remove_cv_t<std::remove_reference_t<MemType>>>();
}

int main() {
    int    x_a;
    double x_b;
    Foo    F;
    Boo    B{1};

    std::cout<< "x_a     :" << class2str(x_a) <<std::endl;
    std::cout<< "x_b     :" << class2str(x_b) <<std::endl;
    std::cout<< "Foo     :" << class2str(F) <<std::endl;
    std::cout<< "Boo     :" << class2str(B) <<std::endl;
    std::cout<< "Foo::A  :" << class2str(&Foo::A) <<std::endl;
    std::cout<< "Foo::b  :" << class2str(&Foo::b) <<std::endl;
    std::cout<< "Boo::A  :" << class2str(&Boo::A) <<std::endl;
};

Output:

x_a     :Fixed Point
x_b     :Floating
Foo     :Class Foo
Boo     :Class Boo
Foo::A  :Floating
Foo::b  :Class Boo
Boo::A  :Floating
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
-2

A common trick used in the standard library is to use declval.
It's sort of designed for this exact use-case. Or simpler still

template<typename Class, typename MemType>  
std::string class2str(MemType Class::*){  
    return class2str(*reinterpret_cast<MemType*>(0)); // Never use the value, it's null  
}  
Tiger4Hire
  • 1,065
  • 5
  • 11
  • "Never use the value" ? You are using the value to pass it to `class2str`. I am almost certain that this is undefined behavior – 463035818_is_not_an_ai Aug 17 '21 at 10:56
  • It would be if any function existed with a NAMED parameter. As no version of the function actually names the parameter (and so cannot use the value), it's fine. – Tiger4Hire Aug 17 '21 at 11:00
  • That said, it does require the code-maintainer to know this. You might criticise the fix for being "fragile", but it is not U/B. Anonymous (unnamed) parameters, are implicitly "unused", and so have no behaviour, undefined or not. – Tiger4Hire Aug 17 '21 at 11:31
  • Have you tried actually calling this `class2str` function? Both GCC and Clang segfaults on it. – dfrib Aug 17 '21 at 11:44
  • Arggg.... I'm having a bad day... You are absolutely right. It must deference it as it is pushed on to the stack. It depends on the optimisation settings whether it works or not. – Tiger4Hire Aug 17 '21 at 12:01
  • I'll get my coat. – Tiger4Hire Aug 17 '21 at 12:01
  • how to use `declval` is also not taht obvious, because it can only be used in unevaluated context – 463035818_is_not_an_ai Aug 17 '21 at 12:31
  • Na, I wasn't really paying attention when I answered. It suffers from the same problem, which is the original question wanted to use function overloading, when the real answer is to use templated-functions. (why pass a value you don't want to use). It's a trade-off, either answer a question that wasn't asked (which is the better way) or use code that uses dodgy casting, of values that are never used. I chose poorly :-) – Tiger4Hire Aug 17 '21 at 12:43
  • If your interested, the anonymous parameter way is really useful with template'd-classes, as type deduction is generally good with constructor parameters. You pass nullptr, as a pointer (not a reference) to the type you are interested in, and let type deduction do the rest. It can make template-instantiation far less verbose, as you don't need decltype everywhere. If you ignore the parameter, the fact it's really null is no issue. Not really relevant to the question, I wasn't paying attention. – Tiger4Hire Aug 17 '21 at 12:52