c++ - 为什么 C++ 不尝试使用第二个模板重载?
问题描述
我正在实现一个简单的智能指针类,我决定在完成后制作自己的std::make_unique
/std::make_shared
函数版本以配合它。我创建了这两个重载:
// note: Box<T> is my "unique pointer" type, it has a partial specialization for T[],
// and it works as expected when created outside of these functions
template <class T, class... Args> Box<T> make_box(Args &&... args) {
auto ptr = new T(std::forward<Args>(args)...);
return Box<T>(std::move(ptr));
}
template <class T> Box<T> make_box(std::size_t size) {
auto ptr = new std::remove_extent_t<T>[size];
return Box<T>(std::move(ptr));
}
第一个重载工作得很好,至少在这个例子中:
struct Point3D {
double x, y, z;
Point3D() = default;
Point3D(double x, double y, double z) : x{x}, y{y}, z{z} {};
};
// works exactly as expected, Box is created and does what it's supposed to
auto box = make_box<Point3D>(1.0, 2.0, 3.0);
但是,似乎没有使用数组的重载。如果我尝试使用数组类型 T 调用它,程序将无法编译。以下代码在尝试使用第一个重载时给了我一个错误,甚至没有尝试使用第二个:
// Gives an error about "allocation of incomplete type 'Point3D []'
// from inside a template instantiation of 'make_box<Point3D [], int>'.
// the overload with one template parameter isn't used
auto box = make_box<Point3D[]>(20);
// Note that this works fine, and uses the Box specialization that calls delete[]:
Box<Point3D[]> boxed(new Point3D[20]);
这是什么原因?这两个重载似乎实际上与std::make_unique
LLVMlibc++
和 GNU内部的实现相同libstdc++
。它也在多个编译器上执行此操作(使用 GCC 10.1 和 Clang 10.0.1 进行测试,均使用-std=c++17 -Wall -Wextra -pedantic
.
编辑: Box 类的定义:
template <class T> class Box {
T *m_ptr;
public:
explicit Box(T *&&ptr) : m_ptr{ptr} {}
Box() = delete;
Box(const Box &) = delete;
Box(Box &&other) : m_ptr{other.m_ptr} {}
~Box() { delete m_ptr; }
T &operator*() const { return *m_ptr; }
T *operator->() const { return m_ptr; }
};
template <class T> class Box<T[]> {
T *m_ptr;
public:
explicit Box(T *&&ptr) : m_ptr{ptr} {}
Box() = delete;
Box(const Box &) = delete;
Box(Box &&other) : m_ptr{other.m_ptr} {}
~Box() { delete[] m_ptr; }
T &operator*() const { return *m_ptr; }
T *operator->() const { return m_ptr; }
T &operator[](std::size_t idx) { return m_ptr[idx]; }
};
解决方案
“转发引用”推导出的模板类型参数是贪婪的,这会干扰您的重载决议。
你打电话时:
auto box = make_box<Point3D[]>(20);
这实际上是make_box<T,Args...>
用T = Point32[]
and调用Args = int
- 这被明确地解决为比调用更好的重载make_box<T[]>(std::size_t)
。这是因为它20
是 an 的 PR 值int
,它需要转换才能std::size_t
准确匹配第二个重载。由于重载决议总是倾向于选择不需要转换的重载,所以它选择第一个重载。
解决此问题的方法是使用 SFINAE 来防止在为数组类型Args...
时选择重载。T
这是std::make_unique
为了在 aT[]
和T
type 之间进行选择而完成的。通常实现的方式std::make_unique
是通过 SFINAE 检测何时T
是标量、有界数组或无界数组类型,并相应地呈现重载。
使用这种方法,您的代码可以重写为:
namespace detail {
template <typename T>
struct make_box_result
{
using object = T;
};
template <typename T>
struct make_box_result<T[]>
{
using unbounded_array = T[];
};
template <typename T, std::size_t N>
struct make_box_result<T[N]>
{
using bounded_array = T[N];
};
}
// Only enable 'Args...' overload for non-array types
template <typename T, typename...Args>
Box<typename detail::make_box_result<T>::object>
make_box(Args&&...args);
// Only enable 'size_t' overload for array types (unbounded arrays).
// Prevents the greedy lookup
template <typename T>
Box<typename detail::make_box_result<T>::unbounded_array>
make_box(std::size_t size);
// Disabled for fixed types
template <typename T>
Box<typename detail::make_box_result<T>::bounded_array>
make_box() = delete;
还有其他方法可以 SFINAE 防止这种情况发生;我只是以这种方法为例,因为它也可以防止T[N]
被指定。