在模板评估期间允许隐式类型转换

Allow implicit type conversion during template evaluation

我正在为 C++ 类型编写一个包装器 class,它允许我在构造、访问、修改和销毁包装对象时进行检测。为了使这对原始代码透明,我将隐式转换函数包含回基础类型,但是当包装对象直接传递给模板时这会失败,因为不评估隐式转换。下面是一些演示此问题的代码:

#include <utility>

// simplified wrapper class
template <typename T>
class wrap {
    T t;
public:
    wrap() : t() {}
    wrap(const T& _t) : t(_t) {}
    wrap(T&& _t) : t(std::move(_t)) {}

    constexpr operator T&() { return t; }
    constexpr operator const T&() const { return t; }
};

// an example templated function
template <typename T>
bool is_same(const T& t1, const T& t2) { return t1 == t2;}

// second invocation fails due to template substitution failure
bool problem() {
    wrap<int> w(5);

    return is_same(static_cast<int>(w), 5) && is_same<>(w, 5);
}

我可以通过在每个模板调用站点(如第一次调用所示)对包装变量执行 static_cast 来手动解决此问题,但这不能很好地扩展,因为我正在使用大型代码库。 Similar questions 建议将每个模板函数内联为一个 friend 函数,但这也需要识别和复制每个模板,这不会缩放。

对于如何 (1) 使用模板函数解决此转换问题,或 (2) 以其他方式在没有此问题的情况下在源代码级别检测变量,我将不胜感激。

如果您将“内部”人视为指针,它可以使用指针包装函数。
这不是一个完整的解决方案,但对您来说应该是一个很好的起点(例如,您需要仔细编写复制和移动 ctors)。
你可以玩这个代码in here

免责声明:我从 Andrei Alexandrescu 的 this 演讲中得到了这个想法。

#include <iostream>

using namespace std;


template <typename T>
class WrapperPtr
{
   T * ptr;
public:
  WrapperPtr(const WrapperPtr&){
    // ???
  }
  
  WrapperPtr(WrapperPtr&&) {
    // ???
  }
  
  WrapperPtr(const T & other)
     : ptr(new T(other)) {}
     
  WrapperPtr(T * other)
     : ptr(other) {}
     
  ~WrapperPtr()
  {
      // ????
      delete ptr;
  }

  T * operator -> () { return ptr; } 
  
  T & operator * () { return *ptr; }
  const T & operator * () const { return *ptr; }
  
  bool operator == (T other) const {  other == *ptr; }
  
   operator T () { return *ptr; }
};

// an example templated function
template <typename T>
bool my_is_same(const T& t1, const T& t2) { return t1 == t2;}

bool problem() {
    WrapperPtr<int> w(5);
 
    return my_is_same(static_cast<int>(w), 5) && my_is_same(*w, 5);
}

这个例子的错误在于is_same。它声明它需要两个相同类型的参数,这是它不需要的要求,并且没有要求该类型具有它确实需要的 ==

诚然,通常会发现 C++ 对模板函数的约束不佳,因为否则很难且冗长。作者采取了一条实用的捷径。也就是说,修复is_same接口的方法不是吗?

// C++17 version. Close to std::equal_to<>::operator().
template <typename T, typename U>
constexpr auto is_same(T&& t, U&& u)
    noexcept(noexcept(std::forward<T>(t) == std::forward<U>(u)))
    -> decltype(std::forward<T>(t) == std::forward<U>(u))
{
    return std::forward<T>(t) == std::forward<U>(u);
}

经过更正 is_same,代码正常工作。

还有其他可以想象的例子,可能需要两个参数具有相同的类型。例如,如果 return 类型取决于参数类型并且 return 值可以来自:

template <typename T>
T& choose(bool choose_left, T& left, T& right) {
    return choose_left ? left : right;
}

这种情况要少得多。但它实际上可能需要考虑来决定是使用底层类型还是包装类型。如果您在包装器类型中具有这种增强行为,并且有条件地使用包装值或底层值,是否应该包装底层值以继续获得增强行为,或者我们是否放弃增强?即使你能让它默默地选择这两种行为中的一种,你愿意吗?

但是,您仍然可以使获取值比说 static_cast<T>(...) 更容易,例如通过提供访问器:

// given wrap<int> w and int i
is_same(w.value(), 5);
choose_left(true, w.value(), i);

我还有一些重要的意见:

wrap() : t() {}

这要求 T 是默认可构造的。 = default 做正确的事。

wrap(const T& _t) : t(_t) {}
wrap(T&& _t) : t(std::move(_t)) {}

这些并不明确。 T 可以隐式转换为 wrap<T> ,反之亦然。这在 C++ 中效果不佳。例如,true ? w : i 格式不正确。这导致 std::equality_comparable_with<int, wrap<int>> 为假,这将是 is_same 的合理要求。包装类型可能应该显式构造,并且可以隐式转换为基础类型。

constexpr operator T&() { return t; }
constexpr operator const T&() const { return t; }

这些不是引用限定的,因此它们 return 左值引用,即使包装器是右值。这似乎是不明智的。

最后,构造和转换只考虑确切的类型T。但是任何使用 T 的地方,它都可能从其他类型隐式转换而来。在 C++ 中不允许进行两次转换。因此,对于包装器类型,需要做出决定,这通常意味着允许从 T 可构造的任何东西构造。