Why not be insane?
enum class subset_type {
include, all
};
struct include_all_t { constexpr include_all_t() {} };
constexpr include_all_t include_all {};
template<class E>
struct subset {
subset_type type = subset_type::include;
std::variant<
std::array<E, 0>, std::array<E, 1>, std::array<E, 2>, std::array<E, 3>, std::array<E, 4>,
std::vector<E>
> data = std::array<E,0>{};
// check if e is in this subset:
bool operator()( E e ) const {
// Everything is in the all subset:
if(type==subset_type::all)
return true;
// just do a linear search. *this is iterable:
for (E x : *this)
if (x==e) return true;
return false;
}
// ctor, from one, subset of one:
subset(E e):
type(subset_type::include),
data(std::array<E, 1>{{e}})
{}
// ctor from nothing, nothing:
subset() = default;
// ctor from {list}, those elements:
subset(std::initializer_list<E> il):
type(subset_type::include)
{
populate(il);
}
// ctor from include_all:
subset(include_all_t):type(subset_type::all) {}
// these 3 methods are only valid to call if we are not all:
E const* begin() const {
return std::visit( [](auto&& x){ return x.data(); }, data );
}
std::size_t size() const {
return std::visit( [](auto&& x){ return x.size(); }, data );
}
E const* end() const {
return begin()+size();
}
// this method is valid if all or not:
bool empty() const {
return type!=subset_type::all && size()==0;
}
// populate this subset with the contents of srcs, as iterables:
template<class...Src>
void populate(Src const&...srcs) {
std::size_t count = (std::size_t(0) + ... + srcs.size());
// used to move runtime count to compiletime N:
auto helper = [&](auto N)->subset& {
std::array<E, N> v;
E* ptr = v.data();
auto add_src = [ptr](auto& src){
for (E x:src)
*ptr++ = x;
};
(add_src(srcs),...);
this->data = v;
};
// handle fixed size array cases:
switch(count) {
case 0: return helper(std::integral_constant<std::size_t, 0>{});
case 1: return helper(std::integral_constant<std::size_t, 1>{});
case 2: return helper(std::integral_constant<std::size_t, 2>{});
case 3: return helper(std::integral_constant<std::size_t, 3>{});
case 4: return helper(std::integral_constant<std::size_t, 4>{});
default: break;
};
// otherwise use a vector:
std::vector<E> vec;
vec.reserve(count);
auto vec_helper = [&](auto&& c){
for (E x:c) vec.push_back(c);
};
(vec_helper(srcs), ...);
data = std::move(vec);
}
// because what is a set without a union operator?
friend subset& operator|=( subset& lhs, subset const& rhs ) {
if (lhs.type==subset_type::all) return lhs;
if (rhs.type==subset_type::all) {
lhs.type = subset_type::all
return lhs;
}
populate( lhs, rhs );
return lhs;
}
friend subset operator|( subset lhs, subset const& rhs ) {
lhs |= rhs;
return std::move(lhs);
}
};
C++17 and probably typos.
std::vector<Person> GetPeopleOfAge(subset<Age> age)
you can call it with Age::Eleven
, or with include_all
, or with {}
for none, or with {Age::Eleven, Age::Twelve}
for two.
It uses a small buffer optimization to handle up to 4 elements.
If not in all
mode, you can iterate over the elements in the subset using range-based for loops.
Adding support for operator&
, subset_type::none
, subset_type::exclude
, and operator~
left as an exercise.