首页 > 解决方案 > 为什么 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_uniqueLLVMlibc++和 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]; }
};

标签: c++c++11templatesc++17

解决方案


“转发引用”推导出的模板类型参数是贪婪的,这会干扰您的重载决议。

你打电话时:

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[]Ttype 之间进行选择而完成的。通常实现的方式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]被指定。


推荐阅读