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

Posted

技术标签:

【中文标题】使用 C++ 概念重载不同模板类型的运算符【英文标题】:Overload operators on different templated types with C++ concepts 【发布时间】:2021-09-15 13:48:09 【问题描述】:

我正在尝试为不同的模板类型提供算术运算符+-*/(和就地+= 等)的类外定义。我读到 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&lt;n,T&gt; 类的类提出合适的概念时遇到了一些麻烦,但下面的那个似乎可行:

// 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>& lhs, const Vect<n, T>& rhs);
    ...

Error: undefined reference to Vect&lt;10, double&gt; operator+(Vect&lt;10, double&gt; const&amp;, Vect&lt;10, double&gt; const&amp;)' ... ld returned 1 exit status

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

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

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

谢谢!

【问题讨论】:

关于 try 2:模板通常在头文件中声明和定义,否则可能会导致链接器错误(更多关于它here)。 你的 lambda 需要一个明确的static_cast&lt;T&gt;。所以[&amp;] (size_t i) return static_cast&lt;T&gt;(i) / static_cast&lt;T&gt;(n); 或模板化的 lambda。目前u 初始化为0 您不能覆盖非虚拟函数。 @2b-t 感谢您的指点。适合static_cast&lt;T&gt;(T)i / (T)n 也可以 @shevket (T) 的表达力不如 static_cast&lt;T&gt;(...)(T) 在不同的上下文中可能意味着非常不同的东西:See here 【参考方案1】:
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&lt;T(size_t)&gt; 构造并具有[size_t]-&gt;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() 方法要求。

然后我们对所有Vectors写一些操作:

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 找到的,因为它与 Vect 一样位于 MyNS 中。对于MyNS 之外的类型,您必须将using MyNS::operator+ 纳入当前范围。这是故意的,而且几乎是不可避免的。

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

...

TL;DR

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

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

【讨论】:

谢谢!我将不得不更深入地研究它,但我喜欢构造函数签名要求:) @shevket 还要考虑std::declval&lt;IndexResult&lt;V const&amp;&gt;(*)(std::size_t)&gt;()——这可以避免编译器警告“没有返回路径产生值”或其他什么。 您能否提供一些关于文件拆分的提示?我可以将struct Vect ...template &lt;Vector V&gt; ... 拆分为不同的.cpp 或.hpp 文件吗? 我确实得到了error: no return statement in function returning non-void,带有下划线的 V([] (size_t) -&gt; IndexResult&lt;V const&amp;&gt;) 。您能否更准确地了解declval&lt;IndexResult&lt;V const&amp;&gt;(*)(size_t)&gt;() 的放置位置以及这些结构的语法?干杯! 好的,我将构造函数要求更新为 V (declval&lt;...&gt;()) 。我将概念和 operator+ 定义放在文件“alg.h”中,并将其包含在定义 Vect 类的文件“vect.h”中,它似乎可以工作(不是相反,应该从实例中导入概念) .不过我不知道最佳实践...

以上是关于使用 C++ 概念重载不同模板类型的运算符的主要内容,如果未能解决你的问题,请参考以下文章

C++学习:5其他语法

尝试在模板类中重载 / 运算符的 C++ 错误

如何为从C++中的模板继承的类重载赋值运算符

C++模板到底是个啥,看这就透了

模板化一个类,然后重载运算符 (C++)

重载 [] 和 = 运算符以在 C++ 中接受我的模板类的值