Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ preprocessor test if class member exists

Is there an equivalent of #ifdef to test if a member exists in a class so that processing can be done without causing the code to fail the compiler. I have tried template operations but the particular problem has not succeeded.

For example

#if member baseclass.memberA()
  baseclass.memberA().push_back(data);
#else
  doAlternate(data);
#endif

Obviously the above is not valid, but I am trying to discover if something like this has been added to C++11

Note that in the initial setup, there will exist memberA, memberB, memberC, ... each of which will require the push_back. Other members will be added to the baseclass in the future, which is why I want to create a template so that all the cases will compile and process properly even if the current baseclass does not have some of the members (such as memberX). Otherwise, I can just put in the push_back() line with a very simple template.

This is actually the simplest case. There is also the case in which I create an instantiation of the subclass and then push it back into the subclass member.

// Instantiate an element of the Maindata class
::basedata::Maindata maindata;
//Instantiate an element of the Subdata class
::basedata::Subdata subinfo("This goes into the subinfo vector");
// Process some data that is part of the Subdata class
subinfo.contexts(contextInfo);
// Push the instantiated Subdata into the Subdata member of Maindata
maindata.subdata().push_back(subinfo);

Note that both Subdata and subdata() need to be set up so that the appropriate code is implemented. However, if ::basedata::Subdata exists then so will maindata.subdata().

I have already tried various methods using templates and the particular problem has not been solvable with the various answers received. Examples are template instantiation check for member existing in class, C++ class member check if not a template, C++ template for variable type declaration

like image 339
sabbahillel Avatar asked Feb 10 '23 19:02

sabbahillel


2 Answers

This is just another case for void_t.

We need a little helper template Void and define a convenience template type alias void_t.

#include <type_traits>

template<typename...>
struct Void { using type = void; };

template<typename... T>
using void_t = typename Void<T...>::type;

We define the primary template that implements the fallback policy.

template<typename T, typename = void>
struct Helper
{
  static void
  function(T& t)
  {
    std::cout << "doing something else with " << &t << std::endl;
  }
};

And provide a partial specialization for types that support a specific operation, in this case, .data().push_back(int).

template<typename T>
struct Helper<T, void_t<decltype(std::declval<T>().data().push_back(0))>>
{
  static void
  function(T& t)
  {
    std::cout << "pushing back data to " << &t << std::endl;
    t.data().push_back(42);
  }
};

To hide the Helper implementation detail from our clients and to allow type deduction for the template parameters, we can nicely wrap it up.

template<typename T>
void
function(T& t)
{
  Helper<T>::function(t);
}

And this is how our clients use it.

#include <iostream>
#include <vector>

class Alpha
{
public:
  std::vector<int>& data() { return this->data_; }
private:
  std::vector<int> data_ {};
};

class Beta { /* has no data() */ };

int
main()
{
  Alpha alpha {};
  Beta beta {};
  std::cout << "&alpha = " << &alpha << std::endl;
  std::cout << "&beta  = " << &beta << std::endl;
  function(alpha);
  function(beta);
}

Possible output:

&alpha = 0x7ffffd2a3eb0
&beta  = 0x7ffffd2a3eaf
pushing back data to 0x7ffffd2a3eb0
doing something else with 0x7ffffd2a3eaf

Update: How to apply this technique to multiple members

The technique shown above can be applied to any number of members. Let's make up a little example. Say we want to write a template function frobnicate that takes an argument of generic type and if the object has…

  • …a member function incr that takes no arguments, call it,
  • …a data member name, append some text to it if possible and
  • …a data member numbers, push_back some numbers to it if possible.

I really recommend you solve this by implementing three helper structs as shown above. It is not that much redundant typing and makes for much cleaner code.

However, if you wish to ignore this advice, let's see how we can reduce the typing by using a macro. Assuming the same definition of void_t as shown above, we can define the following macro.

#define MAKE_SFINAE_HELPER(NAME, TYPE, OPERATION, ARGS, CODE)           \
  template<typename TYPE, typename = void>                              \
  struct NAME                                                           \
  {                                                                     \
    template<typename... AnyT>                                          \
    void                                                                \
    operator()(AnyT&&...) noexcept                                      \
    {                                                                   \
      /* do nothing */                                                  \
    }                                                                   \
  };                                                                    \
                                                                        \
  template<typename TYPE>                                               \
  struct NAME<TYPE, void_t<decltype(std::declval<TypeT>()OPERATION)>>   \
  {                                                                     \
    void operator()ARGS noexcept(noexcept(CODE))                        \
    {                                                                   \
      CODE;                                                             \
    }                                                                   \
  };

It will define a struct called NAME templated on a type parameter TYPE and define a primary template with an operator () that takes any number of arguments of any type and does absolutely nothing. This is used as the fallback if the desired operation is not supported.

However, if an object of type TYPE supports the operation OPERATION, then the partial specialization with an operator () that takes parameters ARGS and executes CODE will be used. The macro is defined such that ARGS can be a parenthesized argument list. Unfortunately, the preprocessor grammar only allows for a single expression to be passed as CODE. This is not a big problem as we can always write a single function call that delegates to another function. (Remember that any problem in computer science can be solved by adding an extra level of indirection – except, of course, for the problem of too many levels of indirection…) The operator () of the partial specialization will be declared noexcept if and only if CODE is. (This also only works because CODE is restricted to a single expression.)

The reason that the operator () for the primary template is a template is that otherwise the compiler might emit warnings about unused variables. Of course, you can alter the macro to accept an additional parameter FALLBACK_CODE that is placed in the body of the primary template's operator () that should use the same ARGS then.

In the most simple cases, it might be possible to combine the OPERATION and the CODE parameter into one but then CODE cannot refer to ARGS which effectively limits ARGS to a single parameter of type TYPE in which case you could get rid of that parameter as well, if you don't need the flexibility.

So, let's apply this to our problem. First, we need a helper function for pushing back the numbers because this cannot be written (at least, let's pretend this) as a single expression. I make this function as generic as possible, making only assumptions on the member name.

template<typename ObjT, typename NumT>
void
do_with_numbers(ObjT& obj, NumT num1, NumT num2, NumT num3)
{
  obj.numbers.push_back(num1);
  obj.numbers.push_back(num2);
  obj.numbers.push_back(num3);
}

Since the other two desired operations can easily be written as a single expression, we need no further indirection for them. So now, we can generate our SFINAE helpers.

MAKE_SFINAE_HELPER(HelperIncr,
                   TypeT,
                   .incr(),
                   (TypeT& obj),
                   obj.incr())

MAKE_SFINAE_HELPER(HelperName,
                   TypeT,
                   .name += "",
                   (TypeT& obj, const std::string& appendix),
                   obj.name += appendix)

MAKE_SFINAE_HELPER(HelperNumbers,
                   TypeT,
                   .numbers.push_back(0),
                   (TypeT& obj, int i1, int i2, int i3),
                   do_with_numbers(obj, i1, i2, i3))

Equipped with these, we can finally write our frobnicate function. It's really simple.

template<typename T>
void
frobnicate(T& object)
{
  HelperIncr<T>()(object);
  HelperName<T>()(object, "def");
  HelperNumbers<T>()(object, 4, 5, 6);
}

To see that everything works, let's make two structs that partially support the operations in question.

#include <string>
#include <vector>

struct Widget
{
  std::vector<int> numbers {1, 2, 3};
  int counter {};
  void incr() noexcept { this->counter += 1; }
};

struct Gadget
{
  std::string name {"abc"};
  int counter {};
  void incr() noexcept { this->counter += 1; }
};

Since I want to print them, let's also define operators <<.

#include <iostream>

std::ostream&
operator<<(std::ostream& os, const Widget& w)
{
  os << "Widget : { counter : " << w.counter << ", numbers : [";
  int i {};
  for (const auto& v : w.numbers)
    os << (i++ ? ", " : "") << v;
  os << "] }";
  return os;
}

std::ostream&
operator<<(std::ostream& os, const Gadget& g)
{
  os << "Gadget : { counter : " << g.counter << ", "
     << "name = \"" << g.name << "\" }";
  return os;
}

And there we go:

int
main()
{
  Widget widget {};
  Gadget gadget {};
  std::cout << widget << "\n" << gadget << "\n\n";
  frobnicate(widget);
  frobnicate(gadget);
  std::cout << widget << "\n" << gadget << "\n";
}

Output:

Widget : { counter : 0, numbers : [1, 2, 3] }
Gadget : { counter : 0, name = "abc" }

Widget : { counter : 1, numbers : [1, 2, 3, 4, 5, 6] }
Gadget : { counter : 1, name = "abcdef" }

I encourage you to carefully gauge the costs and benefits of this macro approach. In my opinion, the extra complexity is barely worth the small savings on the typing.

like image 63
5gon12eder Avatar answered Feb 13 '23 10:02

5gon12eder


Not with preprocessor, but following may help:

#include <cstdint>

#define DEFINE_HAS_SIGNATURE(traitsName, funcName, signature)               \
    template <typename U>                                                   \
    class traitsName                                                        \
    {                                                                       \
    private:                                                                \
        template<typename T, T> struct helper;                              \
        template<typename T>                                                \
        static std::uint8_t check(helper<signature, &funcName>*);           \
        template<typename T> static std::uint16_t check(...);               \
    public:                                                                 \
        static                                                              \
        constexpr bool value = sizeof(check<U>(0)) == sizeof(std::uint8_t); \
    }

DEFINE_HAS_SIGNATURE(has_memberA, T::memberA, std::vector<int> (T::*)(void));

And then, using SFINAE:

#include <type_traits>

template <typename T>
std::enable_if<has_memberA<T>::value>::type
doBase(T& t, int data)
{
    t.memberA().push_back(data);
}

template <typename T>
std::enable_if<!has_memberA<T>::value>::type
doBase(T& , int data)
{
    doAlternate(data);
}
like image 37
Jarod42 Avatar answered Feb 13 '23 10:02

Jarod42