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\$
5
  • \$\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\$ Feb 6, 2017 at 14:13
  • \$\begingroup\$ [mcve] = Minimal, Complete and Verifiable example, if that's not a term you recognise. \$\endgroup\$ Feb 6, 2017 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\$ Feb 6, 2017 at 14:36
  • \$\begingroup\$ @TobySpeight, I've added a working example to the post ;) \$\endgroup\$ Feb 6, 2017 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, 2017 at 15:02

1 Answer 1

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\$
8
  • \$\begingroup\$ Don't I need to use template <typename Arg>? \$\endgroup\$ 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
    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\$ 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
    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 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, 2017 at 18:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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