C ++ 17限制折叠表达式的类型,用于初始化模板类

时间:2018-01-16 17:27:03

标签: c++ constructor variadic-templates aggregate-initialization fold-expression

我基本上尝试编写自己的游戏引擎用于练习和个人使用(我知道,这几乎是不可能的任务,但正如我所说,它主要用于学习新事物。)

目前,我正在研究我的数学库(主要是矢量和矩阵),我遇到了一个有趣的,但主要是美学问题。

给出以下伪代码:

template <uint8 size>
struct TVector {
    float elements[size];
};

现在我希望能够构造具有所需浮点量作为参数的结构:

TVector<3> vec0(1.0f, 2.5f, -4.0f); 
TVector<2> vec1(3.0f, -2.0f);

TVector<3> vec2(2.0f, 2.2f); // Error: arg missing 
TVector<2> vec3(1.0f, 2.0f, 3.0f) // Error: too many args

由于数组的大小是由模板参数给出的,所以我很难为结构声明一个合适的构造函数。我的最终目标是这样的:

// This is pseudo-ideal-code
TVector(size * (float value)); // Create a constructor with number of size 
                               // parameters, which are all floats

当然,这是非逻辑语法,但我用这种方式最接近的是使用 C ++ 17折叠表达式

template<typename... Args>
    TVector(Args... values) {
        static_assert(sizeof...(values) <= size, "Too many args");
        uint8 i = 0;
        (... , void(elements[i++] = values));
    }

在填充数组的意义上它完全正常并且(我猜)没有太多开销,但对于使用这个结构的程序员来说它也容易出错,因为它没有直接指示有多少构造函数接受的参数。

此外,它没有指定哪个类型的参数,这是我最大的问题。

如果有效,为什么会出现问题?

想象一下,使用以下结构,它使用TVector结构:

template <const uint8 rows, const uint8 columns>
struct TMatrix {
    // elements[-columns-][-rows-]; 
    TVector<rows> elements[columns];
}

给定,构造函数类似于向量结构的fold表达式, 我希望能够使用相应大小的向量或大括号初始化来构造矩阵。

  1. 聚合初始化不起作用。

    TVector<2> vec(1.0f, 3.0f);
    TMatrix<2, 2> mat0(vec, vec); // Works
    TMatrix<2, 2> mat1(vec, {0.2f, -4.2f}); // Error
    // Does not compile, because the Type is not clear
    
  2. 在给定错误参数的情况下,在编译之前不会显示错误 (就像一个尺寸错误的矢量,不适合作为矩阵的列)。

  3. 错误的来源并不总是清楚。

  4. TL; DR:现在终于到了我真正的问题:

    有没有办法限制折叠表达式的类型,最终根本不使用模板并解决上面给出的3个问题?

    我想象的是:

    TVector(float... values) { 
    // Maybe even specify the size of the pack with the size given in the struct template
        uint8 i = 0;
        (... , void(elements[i++] = values));
    }
    

    TMatrix(const TVector<rows>&... values) {
        uint8 i = 0;
        (..., void(elements[i++] = values));
    }
    

    当然,我在这里非常挑剔,这主要是一个美学问题,但我认为这是一个重要的设计决策,可以真正提高代码的可用性。

    感谢您阅读本文并在此处帮助我解答第一个问题:)

2 个答案:

答案 0 :(得分:2)

通过间接,您可以执行以下操作:

template <typename Seq> struct TVectorImpl;

template <std::size_t, typename T> using force_type = T;

template <std::size_t ... Is>
struct TVectorImpl<std::index_sequence<Is...>>
{
    TVectorImpl(force_type<Is, float>... args) : elements{args...} {}

    float elements[sizeof...(Is)];
};

template <std::size_t size>
using TVector = TVectorImpl<decltype(std::make_index_sequence<size>())>;

这避免了也有模板方法(所以构造为{2.4, 5}有效)。

Demo

请注意,它是在C ++ 14中(并且index_sequence可以在C ++ 11中完成)。

答案 1 :(得分:1)

因此,在深入模板元编程和尝试事物后,我遇到了一些解决方案(都有自己的小问题)。

的std :: initializer_list:

<强>优点:

  • 易于实施:

    // Constructors:
    TVector(std::initalizer_list<float> values);
    TMatrix(std::initalizer_list<TVector<rows>> values);
    
  • 支持初始化:

    TVector<3>    vec { 1.0f, 0.0f, 2.0f };
    TMatrix<3, 3> mat { vec, { 3.0f, 4.0f, 1.0f }, vec };
    

<强>缺点:

的std ::阵列

<强>优点:

  • 易于实施:

    // Constructors:
    TVector(std::array<float, size>&& values);
    TMatrix(std::aray<TVector<rows>, columns>&& values);
    
  • 如果数组中的对象是可移动的,则可移动

<强>缺点:

  • Brace初始化非常难看

    TVector<3>    vec { { 1.0f, 0.0f, 2.0f } };
    TMatrix<3, 3> mat { vec, TVector<3>{ { 3.0f, 4.0f, 1.0f } }, vec };
    

折叠表达 - 我最喜欢的解决方案

<强>优点:

  • 没有开销
  • 可移动
  • 使用统一初始化

    TVector<3>    vec { 1.0f, 0.0f, 2.0f };
    TMatrix<3, 3> mat { vec, TVector<3>{ 3.0f, 4.0f, 1.0f }, vec };
    
  • 可以指定构造函数的需要

<强>缺点:

  • 难以实施和指定
  • 在不指定类型的情况下不允许使用嵌套大括号(据我所知)

    // Constructors:
    template<typename... Args, std::enable_if_t<
        is_pack_convertible<float, Args...>::value && 
        is_pack_size_of<columns, Args...>::value, bool> = false >
    TVector(std::array<float, size>&& values);
    
    template<typename... Args, std::enable_if_t<
        is_pack_convertible<Vector<rows>, Args...>::value && 
        is_pack_size_of<columns, Args...>::value, bool> = false >
    TMatrix(std::aray<TVector<rows>, columns>&& values);
    

is_pack_convertible / is_pack_size_of

// Declaration - checks if all types of a pack are convertible to one type
template <typename To, typename... Pack> struct is_pack_convertible;
// End of pack case
template <typename To> struct is_pack_convertible<To> : std::true_type {};
// Recursive bool &&
template <typename To, typename From, typename... Pack>
struct is_pack_convertible<To, From, Pack...> {
    static constexpr bool value = std::is_convertible<From, To>::value
        && is_pack_convertible<To, Pack...>::value;
};

// Declaration - checks if the pack is equal to a certain size
template <size_t size, typename... Pack> struct is_pack_size_of;
// End of pack: size is equal
template <> struct is_pack_size_of<0> : std::true_type {};
// End of pack: size is not equal
template <size_t remainder> struct is_pack_size_of<remainder> : std::false_type {};
// Count down size for every element in pack
template <size_t size, typename Arg, typename... Pack> 
struct is_pack_size_of<size, Arg, Pack...> {
    static constexpr bool value = is_pack_size_of<size - 1, Pack...>::value;
};

我希望这有助于其他人,并在初始化泛型类时简要概述了这些选项。