3

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 } {}  
willem
  • 2,617
  • 5
  • 26
  • 38
  • In c#/java, this would be a breeze with reflection and some decorations. I understand that at present, c++ poses more of a challenge. But since reflection in this case is intrusive by nature anyhow (because the class needs to tell about legal values), perhaps there is a good solution. – willem Mar 28 '19 at 08:54
  • 2
    This is possible with a mixture of macros and templates; someone with more time (and self loathing) than me might want to take a stab at it, but at that point, you're better off just exposing a static `PossibleInstantiations()` function and manually implementing it. – Collin Dauphinee Mar 28 '19 at 09:10
  • Yes, exposing this from SomeClass is a possibility that I thought about. I'd rather externalize this, for reasons that are not in the above question. I'd already be helped if an answer would help me getting this to work with only a single member. – willem Mar 28 '19 at 09:18
  • You can make it private, and friend `PossibleInstantiations` – Caleth Mar 28 '19 at 09:28
  • 1
    Aside: `std::list` is a bad default container. `std::vector` is much more suitable – Caleth Mar 28 '19 at 09:29
  • Is your `ValidValuesForX` supposed to be non-static? – Passer By Mar 28 '19 at 09:53
  • @PasserBy: No they are supposed to be static. Thanks for pointing that out. Edited to reflect. – willem Mar 28 '19 at 09:59

2 Answers2

3

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.

pschill
  • 5,055
  • 1
  • 21
  • 42
  • And that allows too using different types of constructor. – Jarod42 Mar 28 '19 at 10:48
  • As mentioned in comments above, I need the logic for making the combinations to be external to SomeClass. – willem Mar 28 '19 at 11:31
  • @willem I edited my answer and added a non-intrusive version. – pschill Mar 28 '19 at 12:45
  • I would prefer tag dispatching (`template struct Tag{}; std::vector GetPossibleImplementations(Tag)`) instead of specialization. It would allow "partial specialization". – Jarod42 Mar 29 '19 at 18:13
1

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

Jarod42
  • 203,559
  • 14
  • 181
  • 302