Define alias and getter/setter function only if type exists in a class

83 views Asked by At

I am provided with a set of classes that I cannot alter. (For the curious: these are automatically generated from Simulink models) These classes unfortunately make too many things publicly available. So, I decided to write an interface class for all these classes that encapsulates them and makes using them a little safer. However, I am having trouble to write one interface class (template) that works for all possible classes that might occur.

Most of the classes define a structure called MyStruct that I need to use within the interface class. I do not want to use that name, though, for various reasons and would like to use an alias within the interface class instead. So, I ended up with the following code:

// Classes to interface with
class MyClass1
{
public:
    struct MyStruct
    {
        double f1;
        double f2;
    } _myVar {};
};

class MyClass2
{
public:
    struct MyStruct
    {
        double g1;
        double g2;
        double g3;
    } _myVar {};
};

class MyClass3
{};

// Interface class
template<typename T>
class Interface
{
public:
    using MyAlias = typename T::MyStruct;

    void setVar(const MyAlias& newVar) {_object._myVar = newVar;};
    
    void doSomething()
    {
        MyAlias inputs {};
    }
private:
    T _object {};
};


// Main function
int main()
{
    Interface<MyClass1> i1; // compiles fine
    Interface<MyClass2> i2; // compiles fine
    Interface<MyClass3> i3; // compile error: no type named ‘MyStruct’ in ‘class MyClass3’
    return 0;
}

This fails to compile because MyClass3 does not define the type MyStruct. Therefore, the alias cannot be defined.

I tried to look into defining the alias and method only if the type is defined and stumbled upon the SFINAE concept. Unfortunately, I cannot use anything from the standard library in my code. So, things like std::enable_if or std::void_t are off the table. Artificial intelligence came up with the following solutions for the alias and the method:

template<typename U, typename = void>
struct check_mystruct {using type = void; };
template<typename U>
struct check_mystruct<U, decltype((void)U::MyStruct, void())> { using type = typename U::MyStruct; };
using MyAlias = typename check_mystruct<T>::type;
template<typename U = MyAlias>
void setVar(const U& newVar) {_object._myVar = newVar;};
void setVar(...) { /* do nothing */ };

As far as I understand it, this should work. The partial specialization of the check_mystruct struct and the templated version of the method should only be used if S::MyStruct exists. However, if I put it together into the initial program like this:

// Classes to interface with
class MyClass1
{
public:
    struct MyStruct
    {
        double f1;
        double f2;
    } _myVar {};
};

class MyClass2
{
public:
    struct MyStruct
    {
        double g1;
        double g2;
        double g3;
    } _myVar {};
};

class MyClass3
{};

// Interface class
template<typename T>
class Interface
{
public:
    template<typename U, typename = void>
    struct check_mystruct {using type = void; };
    template<typename U>
    struct check_mystruct<U, decltype((void)U::MyStruct, void())> { using type = typename U::MyStruct; };
    
    using MyAlias = typename check_mystruct<T>::type;
    
    template<typename U = MyAlias>
    void setVar(const U& newVar) {_object._myVar = newVar;};
    void setVar(...) { /* do nothing */ };
    
    void doSomething()
    {
        MyAlias inputs {};
    }
private:
    T _object {};
};


// Main function
int main()
{
    Interface<MyClass1> i1; // compiles fine
    Interface<MyClass2> i2; // compiles fine
    Interface<MyClass3> i3; // compiles fine
    
    i1.doSomething(); // compile error: In instantiation of ‘void Interface<T>::doSomething() [with T = MyClass1]’: variable or field ‘inputs’ declared void
    i2.doSomething();
    i3.doSomething();

    return 0;
}

I get a compile error saying

main.cpp: In instantiation of ‘void Interface<T>::doSomething() [with T = MyClass1]’:
main.cpp:58:19:   required from here
main.cpp:44:17: error: variable or field ‘inputs’ declared void
   44 |         MyAlias inputs {};
      |                 ^~~~~~

So, the alias is set to void even though MyClass has a type MyStruct. I do not understand this. Can someone explain what went wrong? Is there another way to achieve what I am trying to do?

3

There are 3 answers

1
tbxfreeware On

So, the alias is set to void even though MyClass has a type MyStruct. I do not understand this. Can someone explain what went wrong?

I was expecting this error to occur [with T = MyClass3], rather than MyClass1. That is when MyAlias becomes an alias for type void. Since you cannot have an object of type void, the declaration of variable inputs will trigger an error.

Is there another way to achieve what I am trying to do?

You could add yet more SFINAE to disable the declaration of variable inputs when MyAlias is type void. There is, however, a much simpler approach. Write a partial specialization for class Interface that is used only when type T has a member type MyStruct.

The implementation below uses a "roll-your-own" definition for void_t that I found on CppReference. This complies the with OP's specification that std::void_t cannot be used.

// main.cpp
namespace tbx
{
    // Definitions for C++11 from CppReference
    // https://en.cppreference.com/w/cpp/types/void_t

    template< typename... Ts>
    struct make_void { typedef void type; };

    template< typename... Ts>
    using void_t = typename tbx::make_void<Ts...>::type;
}

// Classes to interface with
class MyClass1
{
public:
    struct MyStruct
    {
        double f1;
        double f2;
    } _myVar{};
};

class MyClass2
{
public:
    struct MyStruct
    {
        double g1;
        double g2;
        double g3;
    } _myVar{};
};

class MyClass3
{};

// Interface class
// The base template for class `Interface` makes no mention 
// of type `MyStruct`.
template<typename T, typename = void>
class Interface
{
public:
    void doSomething()
    {}
private:
    T _object{};
};

// Interface class
// The partial specialization will be selected when type `T` 
// defines member type `MyStruct`.
template<typename T>
class Interface<T, typename tbx::void_t<typename T::MyStruct>>
{
public:
    using MyAlias = typename T::MyStruct;

    void setVar(const MyAlias& newVar) { _object._myVar = newVar; };

    void doSomething()
    {
        MyAlias inputs{};
    }
private:
    T _object{};
};

// Main function
int main()
{
    Interface<MyClass1> i1; // compiles fine
    Interface<MyClass2> i2; // compiles fine
    Interface<MyClass3> i3; // compiles fine
    return 0;
}
// end file: main.cpp

Enable individual members of the Interface

In his comment below, the OP indicates that he has three different MyStruct types that are part of the Interface: MyStructA, MyStructB, and MyStructC.

The following program shows a variation of class template Interface in which each member function can be enabled or disabled individually. Note the idiomatic use of typename U = T. This forces the compiler to defer, for instance, the evaluation of typename U::MyStruct inputs{};, until the template is actually instantiated.

The techniques used here serve as an example on how to "turn on and off" different members based on the presence or absence of MyStructA, MyStructB, and MyStructC.

CppReference supplied the code for a "roll-your-own" version of enable_if_t.

// main.cpp
namespace tbx
{
    // Definitions for C++11 from CppReference
    // https://en.cppreference.com/w/cpp/types/void_t

    template< typename... Ts>
    struct make_void { typedef void type; };

    template< typename... Ts>
    using void_t = typename tbx::make_void<Ts...>::type;

    // Also from CppReference
    // https://en.cppreference.com/w/cpp/types/enable_if

    template<bool B, class T = void>
    struct enable_if {};

    template<class T>
    struct enable_if<true, T> { typedef T type; };

    template< bool B, class T = void >
    using enable_if_t = typename enable_if<B, T>::type;
}

// has_MyStruct_v
template< typename T, typename = void >
struct has_MyStruct {
    static constexpr bool value = false;
};
template< typename T >
struct has_MyStruct<T, tbx::void_t<typename T::MyStruct>> {
    static constexpr bool value = true;
};
template< typename T >
static constexpr bool has_MyStruct_v = has_MyStruct<T>::value;

// Classes to interface with
class MyClass1
{
public:
    struct MyStruct
    {
        double f1;
        double f2;
    } _myVar{};
};

class MyClass2
{
public:
    struct MyStruct
    {
        double g1;
        double g2;
        double g3;
    } _myVar{};
};

class MyClass3
{};

// Interface class
template<typename T>
class Interface
{
public:
    template< typename U = T, typename = tbx::enable_if_t<has_MyStruct_v<U>>>
    using MyAlias = typename U::MyStruct;

    template< typename U = T, typename = tbx::enable_if_t<has_MyStruct_v<U>>>
    void setVar(const MyAlias<U>& newVar) { _object._myVar = newVar; };

    template< typename U = T, typename = tbx::enable_if_t<has_MyStruct_v<U>>>
    void setVar2(const typename U::MyStruct& newVar) { _object._myVar = newVar; };

    template< typename U = T, typename = tbx::enable_if_t<has_MyStruct_v<U>>>
    void doSomething()
    {
        MyAlias<U> inputs{};             // this works
        typename U::MyStruct inputs2{};  // and this, too!
    }
private:
    T _object{};
};

// Main function
int main()
{
    Interface<MyClass1> i1; // compiles fine
    Interface<MyClass2> i2; // compiles fine
    Interface<MyClass3> i3; // compiles fine

    i1.doSomething();
    i2.doSomething();

    i1.setVar({});
    i2.setVar({});

    i1.setVar2({});
    i2.setVar2({});
    return 0;
}
// end file: main.cpp
0
Ted Lyngmo On

If you can use , you could use Boost.PFR to create individual getters for all the fields in _myVar and specify exactly for what fields there should be setters.

In case T doesn't have a MyStruct, MyAlias will alias a struct with no member variables instead. Of course, no getters and setters will then be useable, but the rest of the functions that doesn't depend on MyStruct can still be used.

#include <boost/pfr.hpp>

template <class T, std::size_t... S>
class Interface {
    template <class, class = void>
    struct get_alias_def { struct type {}; }; // for T's without MyStruct

    template <class A>
    struct get_alias_def<A, std::void_t<typename A::MyStruct>> {
        using type = A::MyStruct;
    };

public:
    using MyAlias = typename get_alias_def<T>::type;

    static constexpr size_t field_count = boost::pfr::tuple_size_v<MyAlias>;

    template <std::size_t F>
    using field_type = boost::pfr::tuple_element_t<F, MyAlias>;

    static_assert(((S < field_count) && ...), "Setter index out of bounds");

    template <std::size_t F> // one getter per field
    const auto& get() const {
        static_assert(F < field_count, "F is out of bounds");
        return boost::pfr::get<F>(_object._myVar);
    }

    // setters for the fields specified in S...
    template <std::size_t F>
    void set(const field_type<F>& value) {
        static_assert(((F == S) || ...), "No setter for field");
        boost::pfr::get<F>(_object._myVar) = value;
    }

    template <std::size_t F>
    void set(field_type<F>&& value) {
        static_assert(((F == S) || ...), "No setter for field");
        boost::pfr::get<F>(_object._myVar) = std::move(value);
    }

    void doSomething() { /*...*/ }

private:
    T _object{};
};

Usage example:

int main() {
    Interface<MyClass3> i3;
    // i3.get<0>();                    // error: no fields available

    Interface<MyClass2, 1, 2> i2;      // only field 1 and 2 has setters
    i2.set<1>(3.141);                  // ok, setter defined
    std::cout << i2.get<1>() << '\n';  // 3.141

    // i2.set<0>(3.141);               // error, "No setter for field"
}

Demo

1
Jarod42 On

With C++20, and specialization with concept, you might do

// Interface class
template <typename T>
class Interface
{
public:
    void doSomething() {}
    // ...
private:
    T _object {};
};

template<typename T>
requires (requires {
    typename T::MyStruct;
})
class Interface<T>
{
public:
    
    using MyAlias = typename T::MyStruct;
    
    void setVar(const MyAlias& newVar) {_object._myVar = newVar;};
    
    void doSomething()
    {
        [[maybe_unused]]MyAlias inputs {};
    }
private:
    T _object {};
};

Demo