10
\$\begingroup\$

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;
}
\$\endgroup\$
  • \$\begingroup\$ Doesn't compile for me (as C++17), even after adding #include <vector> - are you missing declarations of V and K 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\$ – Toby Speight Feb 6 '17 at 14:13
  • \$\begingroup\$ [mcve] = Minimal, Complete and Verifiable example, if that's not a term you recognise. \$\endgroup\$ – Toby Speight Feb 6 '17 at 14:20
  • 3
    \$\begingroup\$ @TobySpeight On Code Review, we don't really want MCVEs. In particular, "minimal" is problematic, since we require the code to be real code from a project. \$\endgroup\$ – 200_success Feb 6 '17 at 14:36
  • \$\begingroup\$ @TobySpeight, I've added a working example to the post ;) \$\endgroup\$ – João Pires Feb 6 '17 at 14:37
  • \$\begingroup\$ I have rolled back the last edit. Please see What should I do when someone answers my question? \$\endgroup\$ – t3chb0t Feb 6 '17 at 15:02
10
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Don't I need to use template <typename Arg>? \$\endgroup\$ – João Pires Feb 6 '17 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 Feb 6 '17 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 Feb 6 '17 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 Feb 6 '17 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 typesafe sprintf. You'd want to call it e.g. with format("%d: %s", some_int, some_string) — that won't work well with std::initializer_list. \$\endgroup\$ – celtschk Feb 6 '17 at 18:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.