C++ Alternating between two variables at compile t

2019-08-26 07:26发布

问题:

Suppose you have a class that operates on a vector:

class Foo{
public:
    Foo() {
        m_dynamic_data.push_back(5);
        std::cout << m_dynamic_data[0] << std::endl;
    }
private:
    std::vector<int> m_dynamic_data;
};

In my case this class is huge with 2500 additional lines of code. This class behaves dynamic (hence std::vector). But I would also like to provide a "static" implementation (using std::array). So std::size_t N is added, which now should control when to use which attribute.

template<std::size_t N>
class Foo{
private:
    std::vector<int> m_dynamic_data;  //use this, when N == 0
    std::array<int, N> m_static_data; //use this, when N != 0
};

I am not sure if I can get this to work. using #define won't do the job (since it can't alternate). constexpr can't be wrapped around two attributes either. The best solution is probably to provide a base class and then inherit the dynamic and static case from it. But before I spent the next days doing this, I wonder if there isn't a technique afterall.

I thought about putting both into a std::unique_ptr and only constructing the relevant array:

template<std::size_t N>
class Foo {
public:
    Foo() {
        if constexpr (N) {
            m_static_data_ptr = std::make_unique<std::array<int, N>>();
            (*m_static_data_ptr)[0] = 5;
            std::cout << (*m_static_data_ptr)[0] << std::endl;
        }
        else {
            m_dynamic_data_ptr = std::make_unique<std::vector<int>>(1);
            (*m_dynamic_data_ptr)[0] = 5;
            std::cout << (*m_dynamic_data_ptr)[0] << std::endl;
        }
    }
private:
    std::unique_ptr<std::vector<int>> m_dynamic_data_ptr;
    std::unique_ptr<std::array<int, N>> m_static_data_ptr;
};

I earlier asked about this case here. But apparently this doesn't seem like a good approach. (fragmenting memory, cache miss rate). std::optional also seems interesting, but it pushes the sizeof(Foo) too far for my goal.

Ultimately there is also using void pointers:

template<std::size_t N>
class Foo {
public:
    Foo() {
        if constexpr (N) {
            m_data = malloc(sizeof(std::array<int, N>));
            (*static_cast<std::array<int, N>*>(m_data))[0] = 5;
            std::cout << (*static_cast<std::array<int, N>*>(m_data))[0] << std::endl;
        }
        else {
            m_data = new std::vector<int>;
            (*static_cast<std::vector<int>*>(m_data)).push_back(5);
            std::cout << (*static_cast<std::vector<int>*>(m_data))[0] << std::endl;
        }
    }

    ~Foo() {
        delete[] m_data;
    }
private:
    void* m_data;
};

But this seems pretty dirty [...] So the goal would be to work with either array structure at compile time. Thanks for any help / suggestion!

回答1:

R Sahu's answer is great, but you don't need to access the container indirectly through a struct.

template<std::size_t N>
struct FooData { using type = std::array<int, N>;};

template <>
struct FooData<0> { using type = std::vector<int>; };

template<std::size_t N>
using FooData_t = typename FooData<N>::type;

template<std::size_t N>
class Foo{
   private:
      FooData_t<N> data;
};

Alternatively, you can also use std::conditional_t:

template<std::size_t N>
class Foo{
   private:
      std::conditional_t<N==0, std::vector<int>, std::array<int, N>> data;
};


回答2:

You can abstract the data part of Foo to another class template.

template<std::size_t N> struct FooData
{
   std::array<int, N> container;
}

template <> struct FooData<0>
{
   std::vector<int> container;
}

template<std::size_t N>
class Foo{
   private:
      using DataType = FooData<N>;
      DataType data;
};

You have to add member functions to FooData to support additional abstractions. The number of such functions and their interface depends on how differently you use the containers in Foo.



回答3:

You may want to isolate this dynamic/static "morphing" from the rest of your giant 2500-lines Foo class. I can imagine a tiny wrapper around std::array to mimic the interface of std::vector. It can be used as a member of Foo. If the static capacity is set to the sentinel value 0, then it can be specialized to just derive from a real std::vector:

#include <cassert>
#include <cstddef>

#include <array>
#include <iostream>
#include <vector>

template<class value_type_, std::size_t capacity_>
struct StaticOrDynamic {
  using value_type = value_type_;
  static constexpr std::size_t capacity = capacity_;

  std::array<value_type, capacity> arr_{};
  std::size_t size_{0};

  constexpr void push_back(const value_type& x) {
    assert(size_ < capacity && "must not exceed capacity");
    arr_[size_++] = x;
  }

  constexpr const value_type_& at(std::size_t i) const {
    assert(i < size_ && "must be in [0, size)");
    return arr_[i];
  }

/* other members etc */
};

template<class value_type_>
struct StaticOrDynamic<value_type_, 0>// specialization for dynamic case
  : std::vector<value_type_>
{
  using std::vector<value_type_>::vector;
};

template<std::size_t capacity_>
struct Foo {
  static constexpr std::size_t capacity = capacity_;

  StaticOrDynamic<int, capacity> m_data_{};

  Foo() {// static version may be constexpr (without debug output)
    m_data_.push_back(5);
    std::cout << m_data_.at(0) << std::endl;
  }
};

int main() {
  Foo<5> static_foo{};
  Foo<0> dynamic_foo{};
}

A similar behavior (static/dynamic chosen by a template parameter) is offered in, e.g., the Eigen library. I do not know how it is implemented there.