Reputation: 2717
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
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:
i
conflicts with some value of j
?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
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) {}
};
Upvotes: 2