7

C++(更具体地说,MinGW 的 g++ 实现)正在变得混乱。我有一个数学向量类,其中包含任意数量的任意类型的元素。元素类型和元素数量在编译时指定。

Vector 类在它的构造函数之一和我称之为“resize”运算符之间变得混淆了。resize 运算符允许程序员将一个大小的向量转换为另一个任意大小的向量。如果转换向量的元素多于基向量,则用 1 填充。这是实现:

/*
 * resize operator:
 * T is the type of element the base vector holds
 * N is the number of elements the base vector holds
 * rN is the size of the new vector
 */
template<typename T, unsigned int N, unsigned int rN>
operator Vector<T, rN>() const 
{
    Vector<T, rN> resize;

    for (unsigned int i = 0; i < rN; i++)
    {
        resize[i] = i < N ? this->elements[i] : 1;
    }

    return resize;
}

vector 类还有一个类型安全的可变参数构造函数,它可以采用任意数量的任意元素组合(必须是 T 类型)和任意数量的 Vectors(可以包含任意数量的元素并且必须是 T 类型),所以只要添加到提供的向量中的元素数的裸元素数等于构造向量包含的元素数。

因此这将是有效的:

vec3 foo(vec2(1, 2), 3);

但不是这个。

vec3 bar(vec4(1, 2, 3, 4), 5);

我确保在编译时通过使用计数器递归所有元素来提供正确数量的元素,然后我使用静态断言来确保计数器以向量可以包含的元素数量结束。这通常效果很好,除了以下代码:

vec4 bar(1, 2, 3, 4);
(vec3) bar; //PROBLEM HERE

发生的事情是 C++ 认为 (vec3) bar 正在请求可变参数构造函数,而实际上它应该调用 resize 运算符。我试过让它们明确,但没有奏效。当我有上述代码而不是可变参数构造函数时,如何确保 C++ 使用 resize 运算符?

简而言之,我如何告诉 C++ 使用它:

//resize operator
template<typename T, unsigned int N, unsigned int rN>
Vector<T, N>::operator Vector<T, rN>();

而不是这个:

//constructor
template<typename T, unsigned int N, typename ... Args>
Vector<T, N>::Vector(Args ... arguments);

当我有这个代码时:

(vec3) someVec4;

如果不清楚,则 vec3 和 vec4 定义如下:

typedef Vector<float, 3> vec3;
typedef Vector<float, 4> vec4;

编辑

新闻,大家!即使我使用 static_cast(someVec4) 它仍然使用 vec4 参数调用 vec3 构造函数。我不知道为什么。

另一个编辑

使构造函数显式允许隐式转换工作,但不允许显式转换。也就是说,这段代码有效:

vec3 foo = someVec4;

但是这段代码仍然给我一个静态断言失败:

vec3 foo = static_cast<vec3>(someVec4);

这基本上没有意义,因为我显式地声明了可变参数构造函数,因此不应该在那里调用它。

此外,根据要求,这是一个SSCCE

TL;DR 版本是,当我尝试显式调用类型转换运算符时,我的代码正在调用显式构造函数,但当我尝试隐式调用它时却没有。

4

4 回答 4

4

没有混乱。构造函数总是优先于转换函数,并且在您的情况下,您的类型始终可以从任何类型的参数构造。这是一个大大简化的示例:

struct foo {
    template<typename T>
    foo(T t);
}

template<typename T>
foo::foo(T)
{ static_assert( std::is_same<T, int>::value, "" ); }

注意模板构造函数的声明(我故意将声明与定义分开):通过采用T,可以接受任何类型的初始值设定项。std::is_constructible<foo, T>::value对所有人都成立T,即使 onlyint会产生一个正确的程序。其他类型将static_assert在构造函数实例化时触发。

有一种秘方可以实现你想要的,它的名字叫 SFINAE——希望你以前听说过它。松散地解释一下(如果你没有),如果你将一个潜在的错误从模板的主体移动到声明中的某个地方,那么在重载解决过程中会丢弃会产生这种错误的特化。把它放在代码中:

struct foo {
    template<
        typename T
        , typename std::enable_if<
            std::is_same<T, int>::value
            , int
        >::type...
     >
     foo(T t);
};

这将是前一个人为示例的 SFINAE 版本。有了这样的声明,那么类似的东西foo f = 42.;就不会产生与以前相同的错误。编译器会抱怨,例如没有适当的从doubleto转换foo,就好像构造函数根本不存在一样。这就是我们想要的,因为如果不存在这样的构造函数,那么规则规定将搜索适当的转换运算符。(好吧,在这种情况下这并不是一个很大的帮助,double但没关系。)

请注意,有几种方法可以使用 SFINAE,而这恰好是我最喜欢的一种形式。您可以通过了解 SFINAE 找到其他人。(为了记录,正确使用模板别名并没有那么可怕,它最终看起来像EnableIf<std::is_same<T, int>>...。)

于 2012-08-11T13:01:41.387 回答
3

使您的构造函数显式并使用:

vec4 someVec4; 
// ....
vec3 someVec3 = someVec4;
于 2012-08-11T11:52:20.557 回答
2

查看您的SSCCE,您可以应用一些清理步骤。

通用构造函数模板的大问题是它匹配所有内容,除非非模板构造函数完全匹配。如果你连 cv 限定都错了,通用构造函数模板将被选中。当我遇到类似问题时,建议我添加一个标记值作为第一个参数:

enum my_marker { mark };
//...
template<typename T, unsigned int N>
class Vector
{
    //...
    template<typename ... Args>
    explicit Vector(my_marker, Args ... args);
};
//...
Vector<int, 4>  va( mark, a1, a2 );

您的其他构造函数不会使用此标记,因此现在您可以区分它们。顺便说一句,您与可以T取值的构造函数有另一个重叠:

template<typename T, unsigned int N>
class Vector
{
    //...
    Vector( T empty );
    Vector( std::initializer_list<T> set );
    //...
};
//...
Vector<int, 4>  vb{ 5 };  // always chooses the list ctr
Vector<int, 4>  vc( 6 );  // I think this uses the single-entry ctr.

当您将数组作为函数参数时,默认情况下它将被视为指针,忽略任何大小信息。如果需要保持大小,则必须通过引用传递:

template<typename T, unsigned int N>
class Vector
{
    //...
    Vector( T const (&set)[N] );  // "T set[N]" -> "T *set"
    //...
};
//...
int             aa[ 4 ] = { 1, 2, 3, 4 }, bb[ 3 ] = { 5, 6, 7 };
Vector<int, 4>  vd( aa );  // The new signature won't accept bb.

这种数组到指针的转换可以防止数组直接赋值,但在计算特殊函数时它们是隐式可赋值的。这意味着不需要您的赋值运算符;默认代码将做正确的事情。

你听说过迭代器吗?如果是这样,那么使用这些加上委托构造函数、标准算法和初始化程序可以减少您的代码。

#include <algorithm>
#include <cassert>
#include <initializer_list>

enum mark_t  { mark };

template< typename T, unsigned N >
class Vector
{
    // The "set" functions are unnecessary, see below.
public:
    // The automatically defined copy-ctr, move-ctr, copy-assign, and
    // move-assign are OK.

    T elements[N];

    Vector()  : elements{}  {}
    // Vector()  : Vector( T{} )  {}  // ALTERNATE
    // Can be removed if following constructor uses a default argument.

    Vector(T empty)
    // Vector(T empty = T{})  // ALTERNATE
    { std::fill( elements, elements + N, empty ); }

    Vector(T const (&set)[N])
    { std::copy( set, set + N, elements ); }

    Vector(std::initializer_list<T> set)
        : elements{}
    {
        assert( set.size() <= N );
        std::copy( set.begin(), set.end(), elements );
        // If you were willing to use std::for_each, why not use a more
        // appropriate algorithm directly?  The lambda was overkill.
        // WARNING: there's an inconsistency here compared to the cross-
        // version constructor.  That one fills unused spots with ones,
        // while this one does it with zeros.
        // WARNING: there's an inconsistency here compared to the single-
        // value constructor.  That one fills all elements with the same
        // value, while this one uses that value for the first element but
        // fills the remaining elements with zeros.
    }

    template<typename ... Args>
    explicit Vector( mark_t, Args ... args)
        : elements{ args... }
        //: elements{ static_cast<T>(args)... }  // ALTERNATE
    {}
    // Array members can now be directly initialized in the member part
    // of a constructor.  They can be defaulted or have each element
    // specified.  The latter makes the private "set" methods unnecessary.
    // The compiler will automatically issue errors if there are too
    // many elements for the array, or if at least one "Args" can't be
    // implicitly converted to "T", or if you have less than "N" elements
    // but "T" doesn't support default-initialization.  On my system, the
    // example "main" flags int-to-float conversions as narrowing and post
    // warnings; the alternate code using "static_cast" avoids this.

    template < unsigned R >
    explicit Vector( Vector<T, R> const &v )
        : Vector( static_cast<T>(1) )
    { std::copy( v.elements, v.elements + std::min(R, N), elements ); }

    T &operator [](unsigned int param)
    { return this->elements[param]; }
    const T &operator [](unsigned int param) const
    { return this->element[param]; }
};

typedef Vector<float, 2> vec2;
typedef Vector<float, 3> vec3;
typedef Vector<float, 4> vec4;

int main()
{
    vec4 someVec4(mark, 1, 2, 3, 4);
    vec3 foo = static_cast<vec3>(someVec4);

    return 0;
}
于 2012-08-14T15:42:37.957 回答
1

我认为让您的代码正常工作的最简单方法是用转换构造函数替换转换运算符。由于该构造函数比可变参数构造函数更专业,因此它应该始终具有优先权。

于 2012-08-11T13:53:35.160 回答