【问题标题】:C++ class template constructor -- overload reference(U&) with array(U*) failedC++ 类模板构造函数 -- 使用数组 (U*) 重载引用 (U&) 失败
【发布时间】:2016-08-17 15:34:46
【问题描述】:

我正在尝试构建一个构造函数来将数组作为参数,该参数会重载另一个使用标量的数组。代码如下。

#include <iostream>

template <typename T>
class SmallVec { // This is a 3 dimensional vector class template
public:
    T data[3] = {0}; // internal data of class
    template <typename U>
    explicit SmallVec(const U& scalar) { // if a scalar, copy it to each element in data
        for(auto &item : data) {
            item = static_cast<T>(scalar);
        }
    }
    template <typename U>
    explicit SmallVec(const U* vec) { // if a vector, copy one by one
        for(auto &item : data) {
            item = static_cast<T>(*vec); 
            vec++;
        }
    }
};

int main() {
    float num = 1.2;
    float *arr = new float[3];
    arr[2] = 3.4;
    SmallVec<float> vec1(num); // take num, which works fine
    SmallVec<float> vec2(arr); // !!!--- error happens this line ---!!!
    std::cout << vec1.data[2] << " "  << vec2.data[2] << std::endl;
    return 0;
}

编译器抱怨

error: invalid static_cast from type 'float* const' to type 'float'

显然,vec2(arr) 仍然调用第一个构造函数。但是,如果我删除template &lt;typename U&gt; 并将U 替换为T。该程序运行良好。我应该怎么做才能纠正这个问题?

欢迎提出任何建议!

【问题讨论】:

  • 提示:第一个构造函数被调用两次。
  • @LogicStuff 非常感谢您的及时评论。是的,第一个构造函数被再次调用。这可能意味着数组(或指向数组的指针)是通过引用传递的?但是我应该如何告诉程序找到第二个构造函数,或者我应该如何区分第一个方法中的参数是标量还是数组?请您再给我一些提示好吗?
  • 您必须使用SFINAE 或标签调度。
  • @LogicStuff 或使用explicit SmallVec(U* vec) 而不是explicit SmallVec(const U* vec)。或者使用SmallVec&lt;float&gt; vec2(static_cast&lt;const float *&gt;(arr)); 而不是SmallVec&lt;float&gt; vec2(arr);。
  • @LogicStuff 这很有帮助!非常感谢您告诉我这些要查找的主题! :-)

标签: c++ arrays templates constructor constructor-overloading


【解决方案1】:

以下是使用 SFINAE 获得所需内容的方法:

#include <vector>
#include <map>
#include <string>

using namespace std;

template<class T>
  struct Foo {

    template <class U, typename enable_if<is_pointer<U>::value, int>::type = 0>
      Foo(U u){}

    template <class U, typename enable_if<!is_pointer<U>::value, int>::type = 0>
      Foo(U u){}

  };


int main()
{
  Foo<int> f('a'); // calls second constructor
  Foo<int> f2("a"); // calls first constructor
}

直播:https://godbolt.org/g/ZPcb5T

【讨论】:

  • 编译器确实检测到float* 为第一个,但它检测float 为第二个,然后采用const float * 和重载决议选择第一个,因为这个const不是因为**。
  • @xaxxon 非常感谢您通过 SFIANE 和 std::enable_if 提供解决方案。它可读且清晰。我还在学习C++,之所以这么晚回复,是因为找不到正确的资源来理解模板参数中的= 0部分。如果U 是第一个构造函数中的指针,我看到enable_if&lt;is_pointer&lt;U&gt;::value, int&gt;::type 是int。但是= 0 在其中完成了什么?你能给我一些解释或一些提示/链接吗?对此,我真的非常感激! :-)
  • 它创建了第二个模板参数。具体来说,它是一个非类型(模板参数也可以是整数值),未命名(因为它没有命名,所以你不能使用它)参数,默认值为 0(因为它有一个默认值,调用者不必指定它)。基本上,这是一个“技巧”,让您可以在不影响您的“真实”函数类型的情况下,仅出于进行 SFINAE 检查的目的而输入一些任意代码。通常你会用返回类型来做这件事,但由于它是一个构造函数,你不能这样做,因为你没有指定返回类型。
  • 对于通过 SFINAE 检查的那个,它最终只是 template&lt;class U, int = 0&gt; Foo(...){}
  • 如果你是最前沿的,你可以稍微清理一下:godbolt.org/g/lgX0MI 但 is_pointer_v 仅在 c++17 中 - 尽管你可以自己制作。 enable_if_t 是 c++14
【解决方案2】:

我正在尝试构建一个构造函数以将数组作为参数

(...)

explicit SmallVec(const U* vec) { // if a vector, copy one by one

你不带数组。你拿一个指针,它可能指向一个数组,也可能不指向一个数组,即使它指向一个数组,谁说数组至少有三个元素?这是一个严重的设计缺陷。

C++ 确实允许您通过引用或 const 引用获取原始数组,即使语法很糟糕:

explicit SmallVec(const U (&vec)[3]) {

那么构造函数的实现也不同:

    for(int index = 0; index < 3; ++index) {
        data[index] = static_cast<T>(vec[index]); 
    }

然而,看看main,问题就更深了。您使用new[] 动态分配数组。这已经是一个非常糟糕的主意了。巧合的是,您的示例也错过了delete[]。为什么不使用本地数组呢?

 float arr[3];

这将使您的程序编译并可能正确运行,但您的代码中仍然存在未定义的行为,因为您只将数组的第三个元素设置为有效值;其他两个元素保持未初始化,并且从未初始化的float 读取,即使您只是复制它,也会正式导致未定义的行为。

那就更好了:

 float arr[3] = { 0.0, 0.0, 3.4 };

除此之外,C++11 还邀请您使用std::array,这通常会使事情更安全并改进语法。这是一个完整的例子:

#include <iostream>
#include <array>

template <typename T>
class SmallVec { // This is a 3 dimensional vector class template
public:
    std::array<T, 3> data; // internal data of class
    template <typename U>
    explicit SmallVec(const U& scalar) { // if a scalar, copy it to each element in data
        for(auto &item : data) {
            item = static_cast<T>(scalar);
        }
    }
    template <typename U>
    explicit SmallVec(std::array<U, 3> const& vec) { // if a vector, copy one by one
        for(int index = 0; index < 3; ++index) {
            data[index] = static_cast<T>(vec[index]); 
        }
    }
};

int main() {
    float num = 1.2;
    std::array<float, 3> arr = { 0.0, 0.0, 3.4 };
    SmallVec<float> vec1(num);
    SmallVec<float> vec2(arr);
    std::cout << vec1.data[2] << " "  << vec2.data[2] << std::endl;
    return 0;
}

【讨论】:

  • 感谢您的详细解释!这很有帮助。是的,这是我的错。本来我用arr[3],后来觉得应该给构造函数一个指针,所以改成new[]却忘了delete []。现在我明白了我将指针传递给数组是错误的。是的,函数内部定义的变量将未初始化,我应该避免这些未定义的行为。感谢您指出这一点。 :-) 以后问问题我会小心的。
【解决方案3】:

即使两个构造函数都使用显式说明符并尽量避免类型转换,您应该注意第一个与第二个一样好候选。如果你用 U 代替 float* 你会得到:

显式 SmallVec(const float*& scalar)

这是完全可以接受的,并且会解释编译错误。 您可以通过将第二个构造函数更改为:

template <typename U>
explicit SmallVec(U* const vec) { // if a vector, copy one by one
    U* local = vec;
    for(auto &item : data) {
        item = static_cast<T>(*local);
        local++;
    }
}

不过,我建议一种更明确的方式:

class ScalarCopy {};
class VectorCopy {};

...

template <typename U>
SmallVec(const U& vec, ScalarCopy);

template <typename U>
SmallVec(const U* const vec, VectorCopy); 

并进行显式调用:

SmallVec<float> vec1(num, ScalarCopy());
SmallVec<float> vec2(arr, VectorCopy());

【讨论】:

  • 您正在使用第一个签名更改签名,这并不是很好,因为您允许构造函数修改 vec 的值。
  • 是的。这是更喜欢第二种更明确的解决方案的另一个原因。
  • 感谢您的解释和建议。你说清楚我的错在哪里。 :-)
  • 除了使用标签类,您还可以将构造函数设为私有并添加两个公共静态函数,如SmallVec::fromScalar 和SmallVec::fromVector。两者都会返回 SmallVec&lt;T&gt; 并在内部调用私有构造函数来创建新对象。
猜你喜欢
  • 2014-11-30
  • 2022-07-07
  • 1970-01-01
  • 2016-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多