4

I am trying to write a vector wrapper (indexed_vec) which stores objects of type ValueType but other datastructures (vectors of other types) refer to these by index (because iterators are clearly not stable).

So when objects of ValueType are deleted in indexed_vec, some housekeeping has to be done to keep the indeces in the other other datastructures up to date.

Therefore indexed_vec also stores 2 lambda's which are effectively "subscribers" (Observer Pattern) to index changes.

The code below works, as desired, but the syntax for constructing the instance of indexed_vec seems clumsy.

Are there better alternatives? I investigated deduction guides, but that doesn't work apparently.

A factory function?

(NOTE: the wrappers exposing of the inner vector as a public members will not stay like that, it's just to reduce the code here)


#include <cstddef>
#include <iostream>
#include <vector>

template <typename ValueType, typename DeleteIndexCBType, typename ChangeIndexCBType>
struct indexed_vec {
    indexed_vec(DeleteIndexCBType& dcb_, ChangeIndexCBType& ccb_) : dcb(dcb_), ccb(ccb_) {}

    DeleteIndexCBType dcb;
    ChangeIndexCBType ccb;

    std::vector<ValueType> v;

    std::size_t erase(std::size_t index_to_erase) {
        // TODO handle empty vector
        v[index_to_erase] = v.back();
        v.pop_back();
        dcb(index_to_erase);
        ccb(v.size(), index_to_erase); // NOTE v.size() is NOW one off the end, but that's accurate
        return 1;
    }
};

template <typename T>
void print(const std::vector<T>& v) {
  for (auto& e: v) std::cout << e << " ";
  std::cout << '\n';
}
  

int main() {
    std::string context = "captured context";

    auto delete_subscriber = [&context](std::size_t idx) {
        std::cout << "deleter: " << context << ": " << idx << "\n";
    };
    auto change_subscriber = [&context](std::size_t old_idx, std::size_t new_idx) {
        std::cout << "updater: " << context << ": " << old_idx << " => " << new_idx << "\n";
    };

    // this seems clumsy?
    indexed_vec<std::size_t, decltype(delete_subscriber), decltype(change_subscriber)> v1(
        delete_subscriber, change_subscriber);

    v1.v.reserve(10);
    for (std::size_t v = 10; v != 20; ++v) v1.v.push_back(v);

    print(v1.v);
    v1.erase(3);
    print(v1.v);
}
Oliver Schönrock
  • 1,038
  • 6
  • 11

2 Answers2

4

One easy easy way to simplify the syntax is to wrap the verbose code in a factory function like

template <typename ValueType, typename DeleteIndexCBType, typename ChangeIndexCBType>
auto make_index_vector(const DeleteIndexCBType& dcb_, const ChangeIndexCBType& ccb_)
{
    return indexed_vec<ValueType, DeleteIndexCBType, ChangeIndexCBType>(dcb_, ccb_);
}

Using that allows you to declare v1 like

auto v1 = make_index_vector<std::size_t>(delete_subscriber, change_subscriber);
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • Fair. the old `make_` pattern seems the only way? Accepted. – Oliver Schönrock Jan 27 '23 at 19:48
  • 1
    @OliverSchönrock Pretty much. CTAD is an all or nothing thing. You either specify no template paramters at all and let the compiler do the deduction or you have to specify all of the template parameters. This makes the factory function an easy solution as it lets you get away with just specifying the value type for the vector and everything else gets deduced for you. To use CTAD for this you would have to add another parameter to your constructor that will be the value type of the vector and then you would need to pass a dummy object to the constructor so its type can be deduced. – NathanOliver Jan 27 '23 at 19:51
3

Since you can't supply only one template parameter out of three, a normal deduction guide won't work, but you could package ValueType in a tag.

template <class T> struct ValueTag {};

template <class ValueType, class DeleteIndexCBType, class ChangeIndexCBType>
struct indexed_vec {
    // added constructor for tag:
    indexed_vec(ValueTag<ValueType>, DeleteIndexCBType& dcb_,
                ChangeIndexCBType& ccb_) :
        dcb(dcb_), ccb(ccb_) {}

    //...

Deduction guide:

template<class ValueType, class DeleteIndexCBType, class ChangeIndexCBType>
indexed_vec(ValueTag<ValueType>, DeleteIndexCBType, ChangeIndexCBType) ->
    indexed_vec<ValueType, DeleteIndexCBType, ChangeIndexCBType>;

Usage:

indexed_vec v1(ValueTag<size_t>{}, delete_subscriber, change_subscriber);
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • interesting, hadn't seen that before. upvoted (but I already accepted the make_ way) – Oliver Schönrock Jan 27 '23 at 20:22
  • 1
    @OliverSchönrock Cheers! _"I already accepted the make way"_ - No worries! :-) I'd use that way too, unless in situations where the `ValueType` is actually a template parameter to layer above and I want to just pass it on to the next layer. In those cases, tagging may be a simpler option. It's not a fix for all situations, but an option. – Ted Lyngmo Jan 27 '23 at 20:26