willem
willem

Reputation: 2717

Intrusive reflection like functionality for c++?

I want to create a function template that creates a list of all legal/valid instances of some class. The class itself is somehow informed about the values that each of its members can take. Function template:

template <typename T>
std::list<T> PossibleInstantiations();

Now, if SomeClass somehow contains information about legal instantiations of all its members (in below example, legal instantiations of i are 1,4,5 and legal instantiations of j are 1.0, 4.5), then

PossibleInstantiations<SomeClass>(); 

should yield a list containing elements {SomeClass(1,1.0), SomeClass(1,4.5), SomeClass(4,1.0), SomeClass(4,4.5), SomeClass(5,1.0), SomeClass(5,4.5)}.

Of course, adding extra elements (+ associated valid values) should automatically be handled by PossibleInstantiations.

Someclass would be implemented something like below. In what way should the plumbing be added to client classes (e.g. MyClass), and how should PossibleInstantiations be implemented?

class SomeClass
{
public:
    int i;
    static std::list<int> ValidValuesFori();

    double j;
    static std::list<double> ValidValuesForj();

    SomeClass(int i, double j);

    //int k;
    //static std::list<int> ValidValuesFork();  //could be implemented at some later stage. 

    //Which would mean the constructor becomes:
    //SomeClass(int i, int j, int k)

    //...
    //Extra wiring for pointing out that i and ValidValuesFori belong to each other,
    //and perhaps for pointing out that i is the first element in the constructor, or so?
    //..
};
static std::list<int> SomeClass::ValidValuesFori()
{
    return std::list<int>{1, 4, 5};
    //Other options:
    //std::list<int> ValidValues;
    //for (int i = 0; i < 1000; i++)
    //{
    //    if (i % 3 == 0)
    //      ValidValues.push_back(i);       
    //}
    //return ValidValues;
}
static std::list<double> SomeClass::ValidValuesForj()
{
    return std::list<double>{1.0, 4.5};
}
SomeClass::SomeClass(int i, double j)//or other constructor
    :i{ i }, j{ j } {}  

Upvotes: 3

Views: 162

Answers (2)

pschill
pschill

Reputation: 5599

Why make it hard if you can make it easy? You already say that SomeClass should know which values are allowed for its members. You could make this explicit by moving GetPossibleImplementations() to the class:

class SomeClass
{
public:
    static std::vector<SomeClass> GetPossibleImplementations()
    {
        std::vector<SomeClass> values;
        for (int i : ValidValuesFori())
            for (double j : ValidValuesForj())
                values.push_back(SomeClass(i, j));
        return values;
    }
    // other methods
    // [...]
}

Then you can still add the template function, if you need it:

template <typename T>
std::vector<T> GetPossibleImplementations()
{
    return T::GetPossibleImplementations();
}

Moving the logic into the class has the following advantages:

  • Only the class knows which constructor args are valid, so it makes sense to have the logic there.
  • The cartesian product of valid argument values may not be sufficient for all cases. What if some value of i conflicts with some value of j?
  • You can still move some logic into helper functions, for example, the cartesian product.

There are situations where you cannot change the implementation of SomeClass and you need an external solution. Even in that case, I think you should keep the logic class-specific: Declare the generic function GetPossibleImplementations<T>() as above, but implement it only for the specific classes:

template <typename T>
std::vector<T> GetPossibleImplementations();

template <>
std::vector<SomeClass> GetPossibleImplementations<SomeClass>()
{
    std::vector<SomeClass> values;
    for (int i : SomeClass::ValidValuesFori())
        for (double j : SomeClass::ValidValuesForj())
            values.push_back(SomeClass(i, j));
    return values;
}

The main differences between the two versions is that the first version yields a compile error if the template argument T does not support T::GetPossibleImplementations() and the second version yields a link error if GetPossibleImplementations<T> is not implemented.

Upvotes: 3

Jarod42
Jarod42

Reputation: 218323

With Cartesian product, we might do:

// cartesian_product_imp(f, v...) means
// "do `f` for each element of cartesian product of v..."
template<typename F>
void cartesian_product_imp(F f) {
    f();
}
template<typename F, typename H, typename... Ts>
void cartesian_product_imp(F f, std::vector<H> const& h,
                           std::vector<Ts> const&... vs) {
    for (H const& he: h) {
        cartesian_product_imp([&](Ts const&... ts){
                                  f(he, ts...);
                              }, vs...);
    }
}

template <typename... Ts>
std::vector<std::tuple<Ts...>> cartesian_product(std::vector<Ts> const&... vs) {
    std::vector<std::tuple<Ts...>> res;

    cartesian_product_imp([&](Ts const&... ts){
                              res.emplace_back(ts...);
                          }, vs...);
    return res;
}

template <typename T>
std::vector<T> PossibleInstantiations()
{
    auto validValuesByArgs = T::ValidValuesForArgs();
    auto validArgs =
        std::apply([](const auto&... args){ return cartesian_product(args...); },
                   validValuesByArgs);
    std::vector<T> res;

    std::transform(validArgs.begin(), validArgs.end(),
                   std::back_inserter(res),
                   [](const auto& t){
                       return std::apply([](const auto&... args) { return T(args...); },
                                         t);
                    });
    return res;
}

With

class SomeClass
{
public:
    int i;
    double j;
    const std::string s;

    static std::tuple<std::vector<int>, std::vector<double>, std::vector<std::string>>
    ValidValuesForArgs() { return {{1, 4, 5}, {1.1, 4.5}, {"foo", "bar"}}; }

    SomeClass(int i, double j, const std::string& s) : i(i), j(j), s(s) {}
};

Demo

Upvotes: 2

Related Questions