使用 C++ 概念在不同模板类型上重载运算符

问题描述

我正在尝试为不同的模板类型提供算术运算符 +-*/(以及就地 += 等)的类外定义。我读到 C++20 概念是一个很好的方法,因为可以将输入/输出类型限制为仅提供一个模板化定义,尽管我找不到很多这样的例子......

我使用类型安全向量作为基类:

// vect.cpp
template<size_t n,typename T> 
struct Vect {
    
    Vect(function<T(size_t)> f) {
        for (size_t i=0; i < n; i++) {
            values[i] = f(i);
        }
    }
    
    T values [n];

    T operator[] (size_t i) {
        return values[i];
    }
}

我有一个像这样的张量的派生类:

// tensor.cpp
template <typename shape,typename T>
struct Tensor : public Vect<shape::size,T> {
    // ... same initiliazer and [](size_t i)
}

并且我还将为只读视图/切片定义一个派生类,覆盖 operator [] 以跨步。我想对每个类中的 fmapfold 方法进行硬编码,并尽可能避免复制样板代码

由于不同的模板参数,我在为类似 Vect<n,T> 的类提出合适的概念时遇到了一些麻烦,但下面的方法似乎可行:

// main.cpp
template<typename V,int n,typename T> 
concept Vector = derived_from<V,Vect<n,T>>

template<int n,typename T,Vector<n,T> V>
V operator + (const V& lhs,const V& rhs) {
    return V([&] (int i) {return lhs[i] + rhs[i];});
}

int main () {
    size_t n = 10;
    typedef double T;
    Vect<n,T> u ([&] (size_t i) {return static_cast<T>(i) / static_cast<T>(n);});
    log("u + u",u);
    return 0;
}

Error: template deduction/substitution Failed,Could not deduce template parameter 'n'

尝试 2:

基于 this question,我认为课外定义必须更加冗长,因此我在 vect.cpp添加了几行。

这似乎是人为的,因为它需要 (3 * N_operators) 类型签名定义,其中避免代码重复是提出这个问题的原因。另外,我真的不明白 friend 关键字在这里做什么。

// vect.cpp
template<size_t n,typename T>
struct Vect;

template<size_t n,typename T> 
Vect<n,T> operator + (const Vect<n,T>& lhs,const Vect<n,T>& rhs);

template<size_t n,typename T>
struct Vect {
    ...
    friend Vect operator +<n,T> (const Vect<n,T>& rhs);
    ...
}

Error: undefined reference to Vect<10,double> operator+(Vect<10,double> const&,Vect<10,double> const&)' ... ld returned 1 exit status

我猜编译器在抱怨实现是在 main.cpp 而不是 vect.cpp 中定义的?

问题:正确的 C++ 方法是什么?有什么方法可以让编译器满意,例如带头文件

我真的在这里寻找 DRY 答案,因为我知道代码可以使用大量复制粘贴:)

谢谢!

解决方法

template<int n,typename T,Vector<n,T> V>
V operator + (const V& lhs,const V& rhs) {
  return V([&] (int i) {return lhs[i] + rhs[i];});
}

在这里,您必须有一种方法来推断 nT。您的 V 没有提供; C++ 模板参数推导不会反转非平凡的模板构造(因为这样做通常是 Halt-hard,而是有一个规则,这使得它不可推导)。

看看身体,你不需要 nT

template<Vector V>
V operator + (const V& lhs,const V& rhs) {
  return V([&] (int i) {return lhs[i] + rhs[i];});
}

这是您想要的签名。

下一步是让它发挥作用。

现在,您现有的概念存在问题:

template<typename V,int n,typename T> 
concept Vector = derived_from<V,Vect<n,T>>

这个概念正在查看 V实现,看看它是否源自 Vect

假设有人用 Vect 重写了 Vect2 并使用相同的界面。不应该也是向量吗?

看Vect的实现:

Vect(function<T(size_t)> f) {
    for (size_t i=0; i < n; i++) {
        values[i] = f(i);
    }
}

T values [n];

T operator[] (size_t i) {
    return values[i];
}

它可以由 std::function<T(size_t)> 构造并具有 [size_t]->T 运算符。

template<class T,class Indexer=std::size_t>
using IndexResult = decltype( std::declval<T>()[std::declval<Indexer>()] );

这是一个说明 v[0] 的类型结果是什么的特征。

template<class V>
concept Vector = requires (V const& v,IndexResult<V const&>(*pf)(std::size_t)) {
  typename IndexResult<V const&>;
  { V( pf ) };
  { v.size() } -> std::convertible_to<std::size_t>;
};

我们开始了,Vector 的基于鸭子类型的概念。我添加了一个 .size() 方法要求。

然后我们在所有 Vector 上写一些操作:

template<Vector V>
V operator + (const V& lhs,const V& rhs) {
  return V([&] (int i) {return lhs[i] + rhs[i];});
}
template<Vector V>
std::ostream& operator<<(std::ostream& os,V const& v)
{
    for (std::size_t i = 0; i < v.size(); ++i)
        os << v[i] << ',';
    return os;
}

稍微调整一下你的基地Vect

template<std::size_t n,typename T> 
struct Vect {
    Vect(std::function<T(std::size_t)> f) {
        for (std::size_t i=0; i < n; i++) {
            values[i] = f(i);
        }
    }
    
    T values [n];

    T operator[] (std::size_t i) const { // << here
        return values[i];
    }
    constexpr std::size_t size() const { return n; } // << and here
};

然后这些测试通过:

constexpr std::size_t n = 10;
typedef double T;
MyNS::Vect<n,T> u ([&] (size_t i) {return (T)i / (T)n;});
std::cout << "u + u" << (u+u) << "\n";

Live example

(我正确使用了命名空间,因为当我不使用时我会感到恶心)。

请注意,operator+ 是通过 ADL 找到的,因为它与 MyNS 一样位于 Vect 中。对于 MyNS 之外的类型,您必须将其 using MyNS::operator+ 放入当前范围。这是有意为之,而且几乎不可避免。

(如果您继承自 MyNS 中的某些内容,它也会被找到)。

...

TL;博士

概念通常应该duck typed,这取决于您可以对类型做什么,而不是类型是如何实现的。代码似乎并不关心您是否从特定类型或模板继承,它只是想使用一些方法;所以测试那个

这也避免了尝试将模板参数推导出 Vect 类;我们改为从界面中提取它。