Можно ли определить шаблон класса таким образом, чтобы количество переменных-членов для каждого шаблонного класса зависело от параметра шаблона (т. е. std::size_t)? Я буду называть этот параметр шаблона «размером».
На данный момент я определил набор обычных классов, соответствующих размерности == 2 и размерности == 3.
Для измерения == 2 этот набор классов составляет следующее:
class Scalar2 {}; // a "grade-0" object
class Vector2 {}; // a "grade-1" object
class Bivector2 {}; // a "grade-2" object
class Multivector2
{
public:
Multivector2(Scalar2 scal, Vector2 vec, Bivector2 bivec);
private:
Scalar2 scal;
Vector2 vec;
Bivector2 bivec;
};
Для размерности == 3 мы имеем
class Scalar3 {}; // a "grade-0" object
class Vector3 {}; // a "grade-1" object
class Bivector3 {}; // a "grade-2" object
class Trivector3 {}; // a "grade-3" object
class Multivector3
{
public:
Multivector3(Scalar3 scal, Vector3 vec, Bivector3 bivec, Trivector3 trivec);
private:
Scalar3 scal;
Vector3 vec;
Bivector3 bivec;
Trivector3 trivec;
};
Итак, для измерения n существует n+1 оцениваемых классов (от оценки 0 до оценки n), а затем один мультивекторный класс, содержащий по одной каждой отдельной оценке.
Теперь я хочу распространить это на произвольное количество измерений. Итак, по сути, я хочу что-то вроде этого:
template<std::size_t n /* dimension */, std::size_t k /* grade */>
class K_Vector {};
template<std::size_t n /* dimension */>
class Multivector
{
public:
Multivector(K_Vector<n,0> kvec0, K_Vector<n,1> kvec1, K_Vector<n,2> kvec2, ... K_Vector<n,n> kvecn);
private:
K_Vector<n,0> kvec0;
K_Vector<n,1> kvec1;
K_Vector<n,2> kvec2;
...
K_Vector<n,n> kvecn;
};
Конечно, это не компилируется. Ключевая проблема заключается в том, что количество переменных-членов зависит от значения параметра измерения n. Аналогичную проблему можно также увидеть в отношении определения аргументов конструктора.
Насколько мне известно, не существует способа объявить несколько переменных-членов программно, например, в цикле. В любом случае это было бы не совсем идеально, поскольку присвоение имен отдельным членам было бы довольно проблематично. Итак, хранение членов внутри контейнера, такого как std::vector, звучит как шаг в правильном направлении. Проблема в том, что std::vector требует, чтобы его элементы были одинаковыми. В моем случае каждая переменная-член в Multivector имеет свой тип.
Один из вариантов, который я рассмотрел, — это получить градуированные классы из базового класса и сохранить различные компоненты внутри вектора указателей на базовый:
class K_Vector_Base {};
template<std::size_t n /* dimension */, std::size_t k /* grade */>
class K_Vector : public K_Vector_Base {};
template<std::size_t n /* dimension */>
class Multivector
{
public:
Multivector(std::vector<std::unique_ptr<K_Vector_Base>> kvecs);
private:
std::vector<std::unique_ptr<K_Vector_Base>> kvecs;
};
Однако это не совсем идеально: чтобы избежать нарезки объектов, а поскольку Multivector должен владеть своими градуированными подкомпонентами, нам нужно хранить их как unique_ptrs, что в любом случае кажется пустой тратой хранения классов оценок, которые в любом случае являются легковесными по отдельности. Мультивекторы больше не смогут храниться непрерывно. Более того, этот подход потребует большого количества преобразований от базового к отдельным типам; отдельные градуированные классы слишком различны в том, как представлены их данные и как они участвуют в арифметических операциях, поэтому невозможно использовать преимущества виртуальных методов. например double dot_product(Vector, Vector)
, Vector dot_product(Vector, Bivector)
и double dot_product(Bivector, Bivector)
будут использовать разную логику и возвращать разные типы; это должны быть отдельные бесплатные функции, а не виртуальные функции.
Существуют ли какие-либо подходы, которые могли бы привести к созданию наборов классов, более похожих на размерность == 2 и размерность == 3 в конкретных случаях? (Примечание: ожидается, что классы, соответствующие определенному размерному числу, смогут сосуществовать и взаимодействовать посредством арифметических операций. Классы разных размерностей не будут совместимы друг с другом; вы можете добавить K_Vector<3,1>
к K_Vector<3,2>
(оба измерения 3), но вы не можете добавить K_Vector<3,1>
к K_Vector<2,1>
).
Вы можете использовать std::tuple
для хранения различных K_Vector
:
#include <cstddef>
#include <tuple>
#include <utility>
template <std::size_t N /* dimension */>
class Multivector {
public:
// the storage type:
using m_kvecs_type = decltype([] {
return []<std::size_t... Is>(std::index_sequence<Is...>) {
return std::tuple<K_Vector<N, Is>...>{};
}(std::make_index_sequence<N + 1>{});
}());
template <class... Args> // constructor
Multivector(Args&&... args) : m_kvecs{std::forward<Args>(args)...} {}
// getters:
template <size_t I>
auto& get() { return std::get<I>(m_kvecs); }
template <size_t I>
const auto& get() const { return std::get<I>(m_kvecs); }
template <class T>
auto& get() { return std::get<T>(m_kvecs); }
template <class T>
const auto& get() const { return std::get<T>(m_kvecs); }
private:
m_kvecs_type m_kvecs;
};
Пример использования:
int main() {
Multivector<2> mv(K_Vector<2, 0>{}, K_Vector<2, 1>{}, K_Vector<2, 2>{});
auto& kv0 = mv.get<K_Vector<2, 0>>(); // get reference to K_Vector<2, 0>
auto& kv2 = mv.get<2>(); // get reference to K_Vector<2, 2>
}
Как отметил Jarod42, неограниченный конструктор будет соответствовать конструктору, не являющемуся const
копированием. Чтобы ограничить соответствие N + 1
K_Vector
, вы можете добавить дополнительный уровень, где реализация принимает пакет size_t
вместо одного N
:
template <class Seq> class MultivectorImpl;
template <std::size_t... Is>
class MultivectorImpl<std::index_sequence<Is...>> {
public:
static inline constexpr size_t N = sizeof...(Is) - 1;
MultivectorImpl(K_Vector<N, Is>... args) : m_kvecs{std::move(args)...} {}
// getters, same as above
private:
std::tuple<K_Vector<N, Is>...> m_kvecs;
};
template <std::size_t N>
using Multivector = MultivectorImpl<std::make_index_sequence<N + 1>>;
Обратите внимание на неограниченный конструктор с переменным числом вариантов, он будет соответствовать конструктору неконстантной копии.
В этом случае я обычно использую дополнительные слои Демо
@Jarod42 Это очень хороший момент. Я добавил это в ответ. Спасибо!
decltype([] { return []<std::size_t... Is>(std::index_sequence<Is...>) { return std::tuple<K_Vector<N, Is>...>{}; }(std::make_index_sequence<N + 1>{}); }());
можно упростить до decltype([]<std::size_t... Is>(std::index_sequence<Is...>) { return std::tuple<K_Vector<N, Is>...>{}; }(std::make_index_sequence<N + 1>{}));
, хотя я бы, честно говоря, рассмотрел возможность извлечения этого значения в шаблон типа, чтобы избежать лямбда-выражений в неоцененном контексте.
Я не уверен, что понимаю идею этого требования. Может ли этот класс шаблона быть кортежем?