rocambille
rocambille

Reputation: 15996

Property system based on std::any: template type deduction

To implement a property system for polymorphic objects, I first declared the following structure:

enum class access_rights_t
{
    NONE        = 0,
    READ        = 1 << 0,
    WRITE       = 1 << 1,
    READ_WRITE  = READ | WRITE
};

struct property_format
{
    type_index type;

    string name;

    access_rights_t access_rights;
};

So a property is defined with a type, a name and access rights (read-only, write-only or read-write). Then I started the property class as follows:

template<typename Base>
class property : property_format
{

public:

    template<typename Derived, typename T>
    using get_t = function<T(const Derived&)>;

    template<typename Derived, typename T>
    using set_t = function<void(Derived&, const T&)>;

private:

    get_t<Base, any> get_f;
    set_t<Base, any> set_f;

The property is associated to a base type, but may (and will) be filled with accessors associated to an instance of a derived type. The accessors will be encapsulated with functions accessing std::any objects on an instance of type Base. The get and set methods are declared as follows (type checking are not shown here to make the code minimal):

public:

    template<typename T>
    T get(const Base& b) const
    {
        return any_cast<T>(this->get_f(b));
    }

    template<typename T>
    void set(Base& b, const T& value_)
    {
        this->set_f(b, any(value_));
    }

Then the constructors (access rights are set to NONE to make the code minimal):

    template<typename Derived, typename T>
    property(
        const string& name_,
        get_t<Derived, T> get_,
        set_t<Derived, T> set_ = nullptr
    ):
        property_format{
            typeid(T),
            name_,
            access_rights_t::NONE
        },
        get_f{caller<Derived, T>{get_}},
        set_f{caller<Derived, T>{set_}}
    {
    }

    template<typename Derived, typename T>
    property(
        const string& name_,
        set_t<Derived, T> set_
    ):
        property{
            name_,
            nullptr,
            set_
        }
    {
    }

The functions passed as arguments are encapsulated through the helper structure caller:

private:

    template<typename Derived, typename T>
    struct caller
    {
        get_t<Derived, T> get_f;
        set_t<Derived, T> set_f;

        caller(get_t<Derived, T> get_):
            get_f{get_}
        {
        }

        caller(set_t<Derived, T> set_):
            set_f{set_}
        {
        }

        any operator()(const Base& object_)
        {
            return any{
                this->get_f(
                    static_cast<const Derived&>(object_)
                )
            };
        }

        void operator()(Base& object_, const any& value_)
        {
            this->set_f(
                static_cast<Derived&>(object_),
                any_cast<Value>(value_)
            );
        }
    };

Now, considering these dummy classes.

struct foo
{
};

struct bar : foo
{
    int i, j;

    bar(int i_, int j_):
        i{i_},
        j{j_}
    {
    }

    int get_i() const {return i;}

    void set_i(const int& i_) { this->i = i_; }
};

I can write the following code:

int main()
{
    // declare accessors through bar methods

    property<foo>::get_t<bar, int> get_i = &bar::get_i;
    property<foo>::set_t<bar, int> set_i = &bar::set_i;

    // declare a read-write property

    property<foo> p_i{"bar_i", get_i, set_i};

    // declare a getter through a lambda

    property<foo>::get_t<bar, int> get_j = [](const bar& b_){ return b_.j; };

    // declare a read-only property

    property<foo> p_j{"bar_j", get_j};

    // dummy usage

    bar b{42, 24};
    foo& f = b;

    cout << p_i.get<int>(f) << " " << p_j.get<int>(f) << endl;
    p_i.set<int>(f, 43);
    cout << p_i.get<int>(f) << endl;
}

My problem is that template type deduction doesn't allow me to declare a property directly passing the accessors as arguments, as in:

property<foo> p_i{"bar_i", &bar::get_i, &bar::set_i};

Which produces the following error:

prog.cc:62:5: note: template argument deduction/substitution failed:

prog.cc:149:50: note: mismatched types std::function<void(Type&, const Value&)> and int (bar::*)() const

property<foo> p_i{"bar_i", &bar::get_i, set_i};

Is there a way to address this problem while keeping the code "simple"?

A complete live example is available here.

Upvotes: 0

Views: 1483

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

std::function is a type erasure type. Type erasure types are not suitable for deduction.

template<typename Derived, typename T>
using get_t = function<T(const Derived&)>;

get_t is an alias to a type erasure type. Ditto.

Create traits classes:

template<class T>
struct gettor_traits : std::false_type {};

this will tell you if T is a valid gettor, and if so what its input and output types are. Similarly for settor_traits.

So

 template<class T, class Derived>
 struct gettor_traits< std::function<T(Derived const&)> >:
   std::true_type
 {
   using return_type = T;
   using argument_type = Derived;
 }; 
 template<class T, class Derived>
 struct gettor_traits< T(Derived::*)() >:
   std::true_type
 {
   using return_type = T;
   using argument_type = Derived;
 }; 

etc.

Now we got back to the property ctor:

template<class Gettor,
  std::enable_if_t< gettor_traits<Gettor>{}, int> =0,
  class T = typename gettor_traits<Gettor>::return_value,
  class Derived = typename gettor_traits<Gettor>::argument_type
>
property(
    const string& name_,
    Gettor get_
):
    property_format{
        typeid(T),
        name_,
        access_rights_t::NONE
    },
    get_f{caller<Derived, T>{get_}},
    nullptr
{
}

where we use SFINAE to ensure that our Gettor passes muster, and the traits class to extract the types we care about.

There is going to be lots of work here. But it is write-once work.

My preferred syntax in these cases would be:

std::cout << (f->*p_i)();

and

(f->*p_i)(7);

where the property acts like a member function pointer, or even

(f->*p_i) = 7;
std::cout << (f->*p_i);

where the property transparently acts like a member variable pointer.

In both cases, through overload of ->*, and in the second case via returning a pseudo-reference from ->*.

Upvotes: 1

Hayt
Hayt

Reputation: 5370

At the end of this answer is a slightly different approach. I will begin with the general problem though.

The problem is &bar::get_i is a function pointer to a member function while your alias is creating a function object which needs the class as additional template parameter. Some examples:

Non member function:

#include <functional>

void a(int i) {};

void f(std::function<void(int)> func)
{
}

int main()
{
  f(&a);
  return 0;
}

This works fine. Now if I change a into a struct:

#include <functional>

struct A
{
void a(int i) {};
};

void f(std::function<void(int)> func)
{
}


int main()
{
  f(std::function<void(int)>(&A::a));
  return 0;
}

this gets the error:

error: no matching function for call to std::function<void(int)>::function(void (A::*)(int))'

because the std::function object also need the base class (as you do with your alias declaration)

You need a std::function<void(A,int)>

You cannot make your example much better though.

A way to make it a "bit" more easier than your example would maybe be this approach using CRTP.

#include <functional>

template <typename Class>
struct funcPtr
{
  template <typename type>
  using fun = std::function<void(Class,type)>;
};

struct A : public funcPtr<A>
{
void a(int i) {};
};

void f(A::fun<int> func)
{
};


int main()
{
  f(A::fun<int>(&A::a));
  return 0;
}

And each your "derived" classes derives from a funcPtr class which "auto generates" the specific alias declaration.

Upvotes: 0

Related Questions