Is this the correct, most sane way to do variadic templates in C++?
All that this function does is to see if gate has a key, if it has, it adds it to a vector of values, if not, it default constructs an object in place.
This function belongs to a templated class called core_table
where K
is the key type and V
the value type.
template <typename ...Args>
std::vector<V> values_at(Args const & ...args) const {
std::initializer_list<K> input {args...};
std::vector<V> output;
output.reserve(input.size());
for (auto const & e : input) {
auto const it = gate.find(e);
if (it != gate.end()) {
output.emplace_back((*it)->second);
continue;
}
output.emplace_back();
}
return output;
}
I'm asking it because all the examples of variadic templates that I saw until now made use of recursion. I'm still in a state of desbelief that I can just write std::initializer_list<K> input {k, args...};
and iterate over it, I literally didn't knew until now that this was possible - this does make variadic templates much more approachable in C++, at least for me.
So, is this a good example? How can I make it better, more optimized?
Working Example
#include <iostream>
#include <list>
#include <set>
#include <string>
#include <vector>
using std::cout;
using std::endl;
template <typename K, typename V>
class core_table {
using value = std::pair<K, V>;
using chain = std::list<value>;
using iterator = typename chain::iterator;
using const_iterator = typename chain::const_iterator;
struct compare {
using is_transparent = void;
bool operator ()(const_iterator x, const_iterator y) const {
return x->first < y->first;
}
bool operator ()(const_iterator x, K const & k) const {
return x->first < k;
}
bool operator ()(K const & k, const_iterator y) const {
return k < y->first;
}
};
chain data;
std::set<iterator, compare> gate;
public:
core_table() = default;
core_table(core_table && ct) = default;
core_table(core_table const & ct) {
for (auto const & e : ct) {
push(e.first, e.second);
}
}
core_table(std::initializer_list<value> il) {
for (auto const & e : il) {
push(e.first, e.second);
}
}
template <typename Key, typename Value>
core_table & insert(const_iterator cit, Key && k, Value && v) {
auto const it = gate.find(k);
if (it != gate.end()) {
(*it)->second = std::forward<Value>(v);
return *this;
}
gate.emplace(data.emplace(cit, std::forward<Key>(k), std::forward<Value>(v)));
return *this;
}
template <typename Key, typename Value>
core_table & push(Key && k, Value && v) {
return insert(data.end(), std::forward<Key>(k), std::forward<Value>(v));
}
template <typename ...Args>
std::vector<V> values_at(Args const & ...args) const {
std::initializer_list<K> input {args...};
std::vector<V> output;
output.reserve(input.size());
for (auto const & e : input) {
auto const it = gate.find(e);
if (it != gate.end()) {
output.emplace_back((*it)->second);
continue;
}
output.emplace_back();
}
return output;
}
};
int main() {
core_table<std::string, std::string> ct {{"a", "hello"}, {"b", "world"}};
for (auto const & e : ct.values_at("b", "a")) {
cout << e << endl;
}
return 0;
}
1 Answer 1
Yes, it is a good example
Variadic templates are associated with recursion by similarity to functional programming, where an recursion is a simple method of operating on sequences. Direct expansion is akin to a map or fold, or some other higher-order function.
It can be simplified even more
If you have a similar function
/* find value or return default */
template <typename Arg>
V value_at(Arg const & key) const
{
auto const it = gate.find(key);
if (it != gate.end()) {
return it->second;
}
return {};
}
Then you can condense this down into a single expression
return vector<V>({value_at(args)...});
as that expands the parameter pack into multiple calls to value_at
, which goes into vector's std::initializer_list<V>
constructor.
-
\$\begingroup\$ Don't I need to use
template <typename Arg>
? \$\endgroup\$João Pires– João Pires2017年02月06日 14:38:55 +00:00Commented Feb 6, 2017 at 14:38 -
\$\begingroup\$ @DagobertoPires oops, yes. You can also directly return a std::initializer_list<V> instead of a vector, but I don't think that adds anything \$\endgroup\$Caleth– Caleth2017年02月06日 14:39:35 +00:00Commented Feb 6, 2017 at 14:39
-
\$\begingroup\$ What about references? Your code returns a copy of the value instead of a reference. I've updated the post with a minimal working example of my class if you want to check it. Your Arg key is also by value. My class is pretty mutch complete, now I'm focusing of optimizing it to the maximum. Like, I think I don't gain anything by adding std::forward in it, since elements most actually be coppied from a
std::initializer_list
. \$\endgroup\$João Pires– João Pires2017年02月06日 14:42:53 +00:00Commented Feb 6, 2017 at 14:42 -
\$\begingroup\$ You are also copying into the result vector. To have it return references, values_at would need to have a return type of
std::vector<std::reference_wrapper<V>>
, and the default value would have to be a (private, static) member, otherwise you are returning a ref to a temporary. \$\endgroup\$Caleth– Caleth2017年02月06日 14:50:34 +00:00Commented Feb 6, 2017 at 14:50 -
1\$\begingroup\$ Actually I think most variadic templates use recursion in order to deal with lists of objects that may have different types. For example, think of a
format
function that basically acts as typesafesprintf
. You'd want to call it e.g. withformat("%d: %s", some_int, some_string)
— that won't work well withstd::initializer_list
. \$\endgroup\$celtschk– celtschk2017年02月06日 18:23:27 +00:00Commented Feb 6, 2017 at 18:23
#include <vector>
- are you missing declarations ofV
andK
from somewhere else in your code? It's better if you can provide a [mcve], so that we can compile it and suggest changes that are at least superficially correct. \$\endgroup\$