1

I think this question may just be more about my weakness with doing anything nontrivial with templates more than the problem itself, but I would like to implement a template class that is used like so:

MyArray<std::string, 1, 3, 7> a;
a[1] = "hello";
a[3] = "world";
a[7] = "!";

// runtime error
// a[4] = "?";

Under the hood, MyArray<std::string, 1, 3, 7> would basically be equivalent to the following:

class C
{
  private:

    std::string _values[3];

  public:

    std::string & operator[](std::size_t index)
    {
      switch(index)
      {
        case 1: return _values[0];
        case 3: return _values[1];
        case 7: return _values[2];
        default: /* some error condition */
      }
    }
};

(Of course in real life I'm more likely to use enum values than raw integers, but this gets the point across with a minimum of example code.)

Criteria:

  • Needs to work if the indices aren't known at compile time
  • Access should use a switch statement or something equivalent, not a runtime linear search through permitted keys.
  • Don't want a hashmap with potential bucket collisions like std::unordered_map

I'd also be happy with a reference that explains how to write this sort of template code.

Daniel McLaury
  • 4,047
  • 1
  • 15
  • 37
  • About your first point: do you mean the available indices from `MyArray` or the requested indices from e.g. `a[1] = "hello";`? – Quentin Sep 03 '20 at 14:32
  • @Quentin: I mean that something like `int i; std::cin >> i; std::cout << a[i]` should compile. – Daniel McLaury Sep 03 '20 at 14:34
  • 1
    Implementing the switch case is easy but that’s a linear search. Are you looking for a perfect hash? – Daniel Sep 03 '20 at 14:37
  • Without linear requirement... you could use a `std::map`, create the required values through constructor and, run-time, throw an exception accessing to the map through a index corresponding to a non present value. The problems is that `std::map` is (if I remember correctly) O(n log(n)) – max66 Sep 03 '20 at 14:38
  • @max66: Yes, I'm basically looking for a drop-in replacement for `std::unordered_map`. (Which is O(1) on average but has unnecessary overhead if there are hash collisions.) – Daniel McLaury Sep 03 '20 at 14:38
  • @DanielMcLaury - I don't think the problem is at template level; I think the problem is find an adequate container. – max66 Sep 03 '20 at 14:40
  • @max66: Well, the container will necessarily depend on having its valid keys known at compile time. I don't think someone provides such a container already, but if so I'd be happy to use it. – Daniel McLaury Sep 03 '20 at 14:42
  • @Dani: I don't think a switch is going to be implemented as a linear in search in general, although I guess you have a point that that can happen in the worst case. I should edit the question, but I'm fine so long as the generated code is equivalent to that switch statement. – Daniel McLaury Sep 03 '20 at 14:43
  • I saw switch statements implemented as either a linear search, binary search, or a mix of table indexing and binary search. All of those are possible to generate in constexpr (albeit it might be difficult depending on the version). For example a binary search will be std::sort in constexpr, and std::lower_bound at runtime. – Daniel Sep 03 '20 at 14:50
  • @Dani: I'm hoping for something like this, where the compiler just generates a jump table, with a jump to an error state if you pick an invalid index: https://godbolt.org/z/3cjMch (I'm fine with it only working in conditions where the compiler can do something reasonable with it.) – Daniel McLaury Sep 03 '20 at 15:00
  • what's the range of the index? and if you want it to be definitely a single jump, it will cost so much space and even be impossible if index can be too large. – RedFog Sep 03 '20 at 15:03
  • @RedFog: Obviously I'm struggling to put this into words well, but let's say that I'm happy with whatever the compiler would generate for the equivalent switch statement. – Daniel McLaury Sep 03 '20 at 16:04
  • You can't generate a switch from a template parameter pack, but most compilers will turn the equivalent series of `if` into identical code. See https://stackoverflow.com/questions/46278997/variadic-templates-and-switch-statement – Etienne Laurin Sep 03 '20 at 21:23
  • 1
    @AtnNn: I was hoping that would be the case, but it looks like at least g++ will just happily generate a huge linear search for the equivalent if/elseif/else block: https://godbolt.org/z/ecTz19 – Daniel McLaury Sep 03 '20 at 21:25
  • You're right about GCC. However the version of Clang that I tried seems to generate a jump table for the same code. – Etienne Laurin Sep 03 '20 at 22:06
  • @AtnNn: Do you remember what version you tried? I tried the x86-64 clang 10.0.1 and it generated basically the same thing as the g++ did. – Daniel McLaury Sep 03 '20 at 22:08
  • A jump table is an array of machine instructions. It is indexed exactly line an array of data would be indexed. If your keys are sparse, then a jump table won't magically buy you anything over a data array like `{0, "hello", 0, "world", 0, 0, "!"}`. – n. m. could be an AI Sep 03 '20 at 23:10
  • @n.'pronouns'm. I know what a jump table is and I'm happy with one for my use case. I could of course declare something like std::string a[MAX_VALUES] and use it, but I'd prefer something more typesafe. – Daniel McLaury Sep 04 '20 at 00:04
  • @DanielMcLaury that's the version I tested, with your exact code. Do you not see the `jmp qword ptr [8*rcx + .LJTI0_0]` in https://godbolt.org/z/GbG8s7 ? – Etienne Laurin Sep 04 '20 at 01:02
  • What's not typesafe about it? – n. m. could be an AI Sep 04 '20 at 07:03
  • I mean you can do something like [this](https://godbolt.org/z/sP8K7v) but I don't really see a point... – n. m. could be an AI Sep 04 '20 at 09:09

1 Answers1

1

ok, if you don't mind the possible huge space cost, you can do it like this:

template<typename T, size_t Head, size_t... Tails>
class MagicArray{
    std::array<T, 1 + sizeof...(Tails)> data;
    static constexpr size_t max(size_t a, size_t b) noexcept{
        return (a < b) ? b : a;
    }
    template<typename head, typename... tails>
    static constexpr size_t max(head a, tails... b) noexcept{
        if constexpr (sizeof...(b) == 0)
            return a;
        else
            return max(a, max(b...));
    }
    static constexpr size_t value_max = max(Head, Tails...);
    template<size_t addition, size_t I, size_t A, size_t... B>
    static constexpr size_t find() noexcept{
        if constexpr (I == A)
            return addition;
        else if constexpr (sizeof...(B) == 0)
            return value_max;
        else
            return find<addition + 1, I, B...>();
    }
    template<size_t... Is>
    static constexpr std::array<size_t, value_max + 1> generation(std::index_sequence<Is...>*) noexcept{
        return { (find<0, Is, Head, Tails...>())... };
    }
    static constexpr std::array<size_t, value_max + 1> mapping = generation((std::make_index_sequence<value_max + 1>*)nullptr);
public:
    T& operator[](size_t index){
        if (index > value_max || mapping[index] == value_max)
            throw 0;
        else
            return data[mapping[index]];
    }
    T const& operator[](size_t index) const{
        if (index > value_max || mapping[index] == value_max)
            throw 0;
        else
            return data[mapping[index]];
    }
};

int main(){
    MagicArray<std::string, 1, 3, 7> a;
    a[1] = "Hello";
    a[3] = "World";
    a[7] = "!";
    a[9] = "Boooooooooooooooom!";
}
RedFog
  • 1,005
  • 4
  • 10
  • I'm beginning to see that what I want isn't possible (except using macros instead of templates which I'd rather avoid), and this will actually work for my use case (maybe with a fallover to std::unordered_map if things get too sparse), so I'm accepting it. Thanks for the suggestion! – Daniel McLaury Sep 06 '20 at 01:38