3

I want to have a type that both has named members and also is iterable, so one should be able to refer to the members by index, by label or from a for-each loop. One way to realise this would be to use std::unordered_map<std::string,T> with some helper data for the indices. Clearly this would be horribly inefficient as for each member access you need to hash a std::string.

My current attempt looks like this:

// named-array.h
#pragma once

#include <array>
#include <cstddef>

#define NamedArray_KeyDecl(Name, ...) enum class Name : std::size_t { __VA_ARGS__, NUM }

namespace util {
  template <typename K, typename T>
  struct NamedArray {
    static constexpr std::size_t cast(K k) {
      return static_cast<std::size_t>(k);
    }

    std::array<T,cast(K::NUM)> array;
    NamedArray(std::array<T,cast(K::NUM)> a) : array(a) {
    }   
    constexpr T& operator[](K k) {
      return array[cast(k)];
    }   
    constexpr T const& operator[](K k) const {
      return array[cast(k)];
    }   
  };  
}

Which can be used like so:

  struct Gadget {
    int i;
    Gadget(int i) : i(i) {}
    void operator()() const {
      std::cout << "Gadget(" << i << ")\n";
    }   
  };  

  NamedArray_KeyDecl(Test1, a,b,c,d);

  util::NamedArray<Test1,Gadget> gadgets {{0,1,2,3}};
  // for each works:
  for (auto const& gadget: gadgets.array) {
    gadget();
  }
  // named access works:
  gadgets[Test1::b]();
  // access by index works:
  gadgets.array[1]();

Exposing the array member could be avoided by forwarding all interface functions of std::array.

However, an obvious drawback is that

  1. gadgets[Test1::b] is not as pretty as something along the lines of gadgets.member().b and
  2. there is an exposed #define in a c++ header file (which is extremely smelly)

Is there a way to have a named array with the same performance as an std::array?

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • 1
    is it possible - sure it is. Check how does `boost::multi_index` looks like. It has tag type for each index. I suppose this is something you may want – bartop Feb 20 '20 at 08:27
  • @bartop It appears even more boilerplaty. The way it tags its members, as far as I understand it, is by creating a bunch of empty structs. And the by-index access seems all but lightweight. – bitmask Feb 20 '20 at 08:37
  • I am not saying to use `multi_index` but just to use it as an example. Anyway, if you don't want macros I see tag types as only viable option – bartop Feb 20 '20 at 08:46

2 Answers2

4

Minimalistic example of how it could be done:

#include <array>
#include <type_traits>

template<class Tag, class...Tags>
struct position {
};

template<class Tag, class...Tags>
struct position<Tag, Tag, Tags...> {
    constexpr static unsigned value = 0;
};

template<class Tag, class First, class...Tags>
struct position<Tag, First, Tags...> {
    constexpr static unsigned value = 1 + position<Tag, Tags...>::value;
};


template<class T, class...Tags>
class NamedArray {
public:

    template<class U>
    constexpr T& operator[](U tag) {
        return array_[position<U, Tags...>::value];
    }

    constexpr T& operator[](unsigned val) {
        return array_[val];
    }

    template<class U>
    constexpr T& member(U u = U{}) {
        return (*this)[u];
    }
private:
    std::array<T, sizeof...(Tags)> array_;
};

struct tag1{};
struct tag2{};

int main() {

    NamedArray<int, tag1, tag2> a;
    a[tag1{}];
    a[tag2{}];

    a.member(tag1{});
    a.member<tag1>();

} 
bartop
  • 9,971
  • 1
  • 23
  • 54
  • Ah, now I see it. Yes, this is much better. Not optimal (because of the empty structs), but a good solution! – bitmask Feb 20 '20 at 18:46
0

Just define the enums as you want them. And leaving them unscoped is fine, you want the names to leak into the declaration's scope, and are helped by the implicit conversion to std::size_t.

template <typename K, typename T>
using NamedArray = std::array<T, K::NUM>;

Then

struct Gadget {
  int i;
  Gadget(int i) : i(i) {}
  void operator()() const {
    std::cout << "Gadget(" << i << ")\n";
  }   
};  

enum Test1 : std::size_t { a, b, c, d, NUM };

int main() {
    NamedArray<Test1,Gadget> gadgets { 0,1,2,3 };
    // for each works:
    for (auto const& gadget: gadgets) {
      gadget();
    }
    // named access works:
    gadgets[b]();
    // access by index works:
    gadgets[1]();
}
Caleth
  • 52,200
  • 2
  • 44
  • 75