嵌入式现代C++教程——模板友元与 Barton-Nackman 技巧
你有没有想过,为什么标准库的 std::complex 或 std::pair 可以直接用 == 比较?为什么它们不需要在全局作用域定义一堆运算符函数?答案就是——友元注入与 Barton-Nackman Trick。
这是一种优雅的模板技术,它不仅让运算符重载变得简洁,更是 CRTP(奇异递归模板模式)的前身。本章将深入探讨这一机制的原理,并实现一个功能完整的可比较 Point<T> 类型。
友元函数与模板的基本关系
在理解 Barton-Nackman Trick 之前,我们需要先回顾 C++ 中友元函数的基本概念,以及它与模板的结合方式。
普通类的友元函数
对于非模板类,定义友元运算符非常简单:
class Point {
double x, y;
public:
Point(double x, double y) : x(x), y(y) {}
// 友元函数声明
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
friend bool operator!=(const Point& a, const Point& b) {
return !(a == b);
}
};
// 使用
Point p1{1, 2}, p2{3, 4};
bool eq = (p1 == p2); // 正常工作2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这种方式下,友元函数在类内部定义,但属于外部作用域(可以在类外部调用)。
类模板的友元函数困境
当我们把 Point 改成模板时,问题就出现了:
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 尝试定义友元运算符
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
// 使用
Point<int> p1{1, 2};
Point<int> p2{3, 4};
// bool eq = (p1 == p2); // ❌ 链接错误!未定义的引用2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为什么?因为模板类的友元函数本身不是模板,而是针对每个实例化类型生成的非模板函数。如果友元函数定义在类内部,它是内联的,理论上应该能工作。但实践中,某些编译器可能会在链接时出现问题。
更重要的是,如果我们想让 Point<int> 和 Point<double> 也能比较,这种方式就无能为力了。
友元注入机制
现在让我们介绍核心概念——友元注入(Friend Injection)。
什么是友元注入?
友元注入是指:当在类模板内部定义一个友元函数时,这个函数不仅成为类的友元,还会被注入到外围作用域(通常是全局或命名空间作用域),并且可以通过参数依赖查找(ADL)找到。
关键点:这个友元函数不是模板函数,而是一个非模板函数,但它可以访问类的私有成员。
基本语法
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 友元注入:函数在类内定义,但可在外部通过ADL找到
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
// 使用
Point<int> p1{1, 2};
Point<int> p2{1, 2};
// 通过ADL找到operator==
bool eq = (p1 == p2); // ✅ 正常工作
// 等价于 bool eq = operator==(p1, p2);
// 但 operator== 不在全局作用域,只能通过ADL找到2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ADL(参数依赖查找)的关键作用
ADL 是 C++ 名称查找规则的一部分:当调用一个函数时,编译器不仅会在当前作用域查找,还会在参数类型所在的命名空间查找。
namespace geometry {
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 友元函数
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
}
// 使用
geometry::Point<int> p1{1, 2}, p2{1, 2};
// ❌ 如果写 operator==(p1, p2) 会找不到
// ✅ 但写 p1 == p2 可以通过ADL找到
bool eq = (p1 == p2); // ADL在geometry命名空间中查找operator==2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
友元注入的三个关键特性
| 特性 | 说明 | 示例 |
|---|---|---|
| 非模板函数 | 每次实例化生成独立的非模板函数 | Point<int> 生成一个 operator==,Point<double> 生成另一个 |
| 内联定义 | 函数体必须在类内部定义 | 不能在类内声明、类外定义 |
| ADL可查找 | 只能通过参数依赖查找找到 | 直接写 operator== 可能找不到 |
template<typename T>
class Point {
// ...
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
Point<int> p1, p2;
p1 == p2; // ✅ ADL找到
// operator==(p1, p2); // ❌ 可能找不到(取决于编译器)2
3
4
5
6
7
8
9
10
11
Barton-Nackman Trick
现在我们进入本章的核心——Barton-Nackman Trick(也称为 "受限的模板友元注入")。
历史背景
这个技巧由 John Barton 和 Lee Nackman 在 1994 年的著作 Scientific and Engineering C++ 中首次描述。它是最早的约束泛型编程技术之一,是后来 CRTP(奇异递归模板模式)和 C++20 Concepts 的思想源头。
核心思想
在类模板内部定义一个友元函数模板,该函数模板的参数类型受类模板参数约束。
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// Barton-Nackman Trick
// 友元函数模板,约束为只能比较相同类型的Point
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
等等,这和之前的友元注入有什么区别?关键在于:这里的 operator== 是一个函数模板,而非模板函数。
正确的 Barton-Nackman 语法
为了真正定义友元函数模板,我们需要显式声明模板参数:
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 方式1:非模板友元(之前讲过的)
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
// 方式2:真正的 Barton-Nackman - 友元函数模板
template<typename U>
friend bool operator==(const Point<U>& a, const Point<U>& b) {
return a.x == b.x && a.y == b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
但实际上,方式1(非模板友元)在大多数场景下已经足够,并且是现代C++推荐的方式。方式2(真正的函数模板)只有在需要跨类型比较时才需要。
Barton-Nackman 的约束作用
传统 Barton-Nackman Trick 的真正威力在于约束:通过将运算符定义为类的友元,只有当操作数类型匹配类模板时,该运算符才参与重载决议。
template<typename T>
class Point {
T x, y;
public:
// 这个operator==只对Point<T>及其派生类可见
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
// 全局的一个通用operator==
template<typename T, typename U>
bool operator==(const T& a, const U& b) {
return false;
}
Point<int> p{1, 2};
int x = 5;
p == p; // 调用Point的友元operator==
x == p; // 调用通用operator==(Point的友元不匹配)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
简化的现代写法
在现代C++(C++11及以后)中,Barton-Nackman Trick 的核心价值已经减弱,因为我们有更好的技术(如 std::enable_if、C++20 Concepts)。但友元注入的语法仍然简洁实用:
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 现代推荐写法:简洁的友元注入
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
friend bool operator!=(const Point& a, const Point& b) {
return !(a == b);
}
// 其他比较运算符...
friend auto operator<=>(const Point& a, const Point& b) {
if (auto cmp = a.x <=> b.x; cmp != 0) return cmp;
return a.y <=> b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注意:C++20 的三路比较运算符 operator<=> 会自动生成所有其他比较运算符,所以只需要定义它就够了。
Barton-Nackman 与 CRTP 的关系
Barton-Nackman Trick 是 CRTP 的前身。理解两者的关系有助于深入掌握模板元编程。
CRTP:奇异递归模板模式
CRTP 是一种设计模式,派生类将自身作为基类的模板参数:
template<typename Derived>
class Base {
public:
void interface() {
// 编译期将基类指针转换为派生类指针
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
// 具体实现
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
Barton-Nackman 到 CRTP 的演变
早期的 Barton-Nackman Trick 代码看起来像这样:
// Barton-Nackman 原始风格(简化版)
template<typename T>
class Ordered {
public:
friend bool operator<(const T& a, const T& b) {
return a.less(b);
}
friend bool operator>(const T& a, const T& b) {
return b < a;
}
// ...其他运算符
};
class Point : public Ordered<Point> {
double x, y;
public:
bool less(const Point& other) const {
if (x != other.x) return x < other.x;
return y < other.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注意这里 Point 继承自 Ordered<Point>——这就是 CRTP 的核心!
现代实现(使用 CRTP)
template<typename Derived>
class Comparable {
public:
friend bool operator<(const Derived& a, const Derived& b) {
return a.compare(b) < 0;
}
friend bool operator>(const Derived& a, const Derived& b) {
return b < a;
}
friend bool operator<=(const Derived& a, const Derived& b) {
return !(a > b);
}
friend bool operator>=(const Derived& a, const Derived& b) {
return !(a < b);
}
friend bool operator==(const Derived& a, const Derived& b) {
return a.compare(b) == 0;
}
friend bool operator!=(const Derived& a, const Derived& b) {
return !(a == b);
}
};
template<typename T>
class Point : public Comparable<Point<T>> {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
int compare(const Point& other) const {
if (x < other.x) return -1;
if (x > other.x) return 1;
if (y < other.y) return -1;
if (y > other.y) return 1;
return 0;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
两种模式对比
| 特性 | Barton-Nackman Trick | CRTP |
|---|---|---|
| 年代 | 1994年 | 1990年代末 |
| 核心 | 友元注入 | 继承+模板 |
| 运算符位置 | 在类内定义友元函数 | 在基类定义友元函数 |
| 代码复用 | 每个类重复定义 | 基类统一实现 |
| 灵活性 | 较低 | 较高 |
| 现代适用性 | 简单场景够用 | 复杂层次结构推荐 |
选择建议:
- 简单类:直接使用友元注入,不需要 CRTP
- 需要共享大量运算符逻辑:使用 CRTP 基类
- C++20:考虑使用 Concepts 约束的运算符
运算符重载的模板技巧
让我们探讨几种常见的运算符重载模板技巧。
技巧1:友元函数 vs 成员函数
template<typename T>
class Point {
T x, y;
public:
// ❌ 成员函数:不对称,需要 Point == 其他类型 能工作
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
// ✅ 友元函数:对称,两边都能处理隐式转换
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
最佳实践:
- 赋值、下标、调用、箭头:必须是成员函数
- 复合赋值(+=、-=等):通常是成员函数
- 算术、比较、IO:通常是非成员(友元)函数
- 类型转换:必须是成员函数
技巧2:跨类型比较
使用模板友元实现不同类型之间的比较:
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 同类型比较
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
// 跨类型比较(int 和 double 可以比较)
template<typename U>
friend bool operator==(const Point& a, const Point<U>& b) {
return a.x == b.x && a.y == b.y;
}
};
// 使用
Point<int> pi{1, 2};
Point<double> pd{1.0, 2.0};
bool eq = (pi == pd); // ✅ 跨类型比较2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
技巧3:使用 std::common_type 统一返回类型
#include <type_traits>
template<typename T, typename U>
auto add(const T& a, const U& b) -> std::common_type_t<T, U> {
return a + b;
}
// 对于运算符
template<typename T>
class Point {
T x, y;
public:
template<typename U>
auto operator+(const Point<U>& other) const
-> Point<std::common_type_t<T, U>> {
return {x + other.x, y + other.y};
}
};
Point<int> pi{1, 2};
Point<double> pd{3.5, 4.5};
auto result = pi + pd; // Point<double>{4.5, 6.5}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
技巧4:C++20 三路比较运算符
C++20 大大简化了比较运算符的定义:
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 只需要定义一个运算符!
friend auto operator<=>(const Point&, const Point&) = default;
};
// 编译器自动生成:
// ==, !=, <, <=, >, >=2
3
4
5
6
7
8
9
10
11
12
自定义三路比较:
template<typename T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
friend auto operator<=>(const Point& a, const Point& b) {
if (auto cmp = a.x <=> b.x; cmp != 0) return cmp;
return a.y <=> b.y;
}
// 三路比较不会自动生成==,需要单独定义
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
技巧5:约束运算符(C++20 Concepts)
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
class Point {
T x, y;
public:
Point(T x, T y) : x(x), y(y) {}
// 只有满足 Numeric 的类型才能比较
friend auto operator<=>(const Point&, const Point&) = default;
};
Point<int> pi; // ✅
Point<std::string> ps; // ❌ 编译错误2
3
4
5
6
7
8
9
10
11
12
13
14
15
实战:实现可比较的 Point<T>
现在让我们实现一个完整的、可比较的 Point<T> 类型,综合运用本章学到的技巧。
需求定义
我们的 Point<T> 应该:
- 支持任意数值类型(int、float、double等)
- 支持所有比较运算符(==、!=、<、<=、>、>=)
- 支持算术运算符(+、-、*、/)
- 支持流输出运算符(<<)
- 使用友元注入实现
- 提供类型安全的距离计算
完整实现
#include <iostream>
#include <cmath>
#include <type_traits>
#include <compare>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
class Point {
T x_, y_;
public:
// 构造函数
constexpr Point() : x_(0), y_(0) {}
constexpr Point(T x, T y) : x_(x), y_(y) {}
// Getter
constexpr T x() const { return x_; }
constexpr T y() const { return y_; }
// Setter
constexpr void set_x(T x) { x_ = x; }
constexpr void set_y(T y) { y_ = y; }
// ===== 比较运算符 =====
// C++20 三路比较(自动生成所有比较运算符)
constexpr friend auto operator<=>(const Point& a, const Point& b) {
if (auto cmp = a.x_ <=> b.x_; cmp != 0) return cmp;
return a.y_ <=> b.y_;
}
constexpr friend bool operator==(const Point& a, const Point& b) {
return a.x_ == b.x_ && a.y_ == b.y_;
}
// ===== 算术运算符 =====
constexpr friend Point operator+(const Point& a, const Point& b) {
return {a.x_ + b.x_, a.y_ + b.y_};
}
constexpr friend Point operator-(const Point& a, const Point& b) {
return {a.x_ - b.x_, a.y_ - b.y_};
}
constexpr friend Point operator*(const Point& p, T scalar) {
return {p.x_ * scalar, p.y_ * scalar};
}
constexpr friend Point operator*(T scalar, const Point& p) {
return p * scalar;
}
constexpr friend Point operator/(const Point& p, T scalar) {
return {p.x_ / scalar, p.y_ / scalar};
}
// ===== 复合赋值运算符 =====
constexpr Point& operator+=(const Point& other) {
x_ += other.x_;
y_ += other.y_;
return *this;
}
constexpr Point& operator-=(const Point& other) {
x_ -= other.x_;
y_ -= other.y_;
return *this;
}
constexpr Point& operator*=(T scalar) {
x_ *= scalar;
y_ *= scalar;
return *this;
}
constexpr Point& operator/=(T scalar) {
x_ /= scalar;
y_ /= scalar;
return *this;
}
// ===== 流输出运算符 =====
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << '(' << p.x_ << ", " << p.y_ << ')';
}
// ===== 实用方法 =====
// 计算到原点的距离
[[nodiscard]] constexpr double distance_from_origin() const {
return std::hypot(static_cast<double>(x_), static_cast<double>(y_));
}
// 计算到另一个点的距离
[[nodiscard]] constexpr double distance_to(const Point& other) const {
double dx = static_cast<double>(x_ - other.x_);
double dy = static_cast<double>(y_ - other.y_);
return std::hypot(dx, dy);
}
// 点积
[[nodiscard]] constexpr T dot(const Point& other) const {
return x_ * other.x_ + y_ * other.y_;
}
// 叉积(2D中返回标量)
[[nodiscard]] constexpr T cross(const Point& other) const {
return x_ * other.y_ - y_ * other.x_;
}
// 判断是否为零点
[[nodiscard]] constexpr bool is_zero() const {
return x_ == T{} && y_ == T{};
}
};
// ===== 跨类型算术运算 =====
template<Numeric T, Numeric U>
auto operator+(const Point<T>& a, const Point<U>& b) {
using Common = std::common_type_t<T, U>;
return Point<Common>{
static_cast<Common>(a.x()) + static_cast<Common>(b.x()),
static_cast<Common>(a.y()) + static_cast<Common>(b.y())
};
}
template<Numeric T, Numeric U>
auto operator-(const Point<T>& a, const Point<U>& b) {
using Common = std::common_type_t<T, U>;
return Point<Common>{
static_cast<Common>(a.x()) - static_cast<Common>(b.x()),
static_cast<Common>(a.y()) - static_cast<Common>(b.y())
};
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
使用示例
#include <cassert>
#include <iostream>
int main() {
// 基本构造
Point<int> p1{3, 4};
Point<int> p2{1, 2};
// 比较运算符
assert(p1 == p1);
assert(p1 != p2);
assert(p1 > p2); // 按字典序比较
// 算术运算
auto p3 = p1 + p2; // Point<int>{4, 6}
auto p4 = p1 - p2; // Point<int>{2, 2}
auto p5 = p1 * 2; // Point<int>{6, 8}
auto p6 = p1 / 2; // Point<int>{1, 2}
// 复合赋值
Point<int> p7{5, 5};
p7 += p2; // p7 变成 {6, 7}
// 跨类型运算
Point<int> pi{10, 20};
Point<double> pd{1.5, 2.5};
auto mixed = pi + pd; // Point<double>{11.5, 22.5}
// 输出
std::cout << "p1 = " << p1 << '\n'; // p1 = (3, 4)
std::cout << "mixed = " << mixed << '\n'; // mixed = (11.5, 22.5)
// 实用方法
Point<double> origin{0, 0};
Point<double> p{3, 4};
std::cout << "Distance: " << p.distance_from_origin() << '\n'; // 5.0
std::cout << "Dot product: " << p.dot(Point<double>{1, 0}) << '\n'; // 3.0
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
嵌入式优化版本
对于嵌入式环境,我们可能需要更轻量的实现:
#include <cstdint>
template<typename T>
class EmbeddedPoint {
T x_, y_;
public:
constexpr EmbeddedPoint() : x_(0), y_(0) {}
constexpr EmbeddedPoint(T x, T y) : x_(x), y_(y) {}
// 简化的比较(只实现 == 和 <)
constexpr friend bool operator==(const EmbeddedPoint& a, const EmbeddedPoint& b) {
return a.x_ == b.x_ && a.y_ == b.y_;
}
constexpr friend bool operator<(const EmbeddedPoint& a, const EmbeddedPoint& b) {
return (a.x_ < b.x_) || (a.x_ == b.x_ && a.y_ < b.y_);
}
// 内联算术运算
constexpr EmbeddedPoint operator+(const EmbeddedPoint& other) const {
return {static_cast<T>(x_ + other.x_), static_cast<T>(y_ + other.y_)};
}
// 饱和加法(避免溢出)
constexpr EmbeddedPoint saturated_add(const EmbeddedPoint& other) const {
if constexpr (std::is_unsigned_v<T>) {
T new_x = (x_ > std::numeric_limits<T>::max() - other.x_)
? std::numeric_limits<T>::max()
: x_ + other.x_;
T new_y = (y_ > std::numeric_limits<T>::max() - other.y_)
? std::numeric_limits<T>::max()
: y_ + other.y_;
return {new_x, new_y};
} else {
return *this + other; // 有符号类型暂不支持
}
}
// 快速距离平方(避免浮点运算)
constexpr T distance_squared() const {
return x_ * x_ + y_ * y_;
}
// 判断点是否在矩形内
constexpr bool is_inside(T left, T top, T right, T bottom) const {
return x_ >= left && x_ <= right && y_ >= top && y_ <= bottom;
}
};
// 使用场景:图形界面、触摸屏检测
using ScreenPoint = EmbeddedPoint<int16_t>;
// 检测触摸点是否在按钮区域内
constexpr bool is_touch_in_button(ScreenPoint touch, int16_t btn_x,
int16_t btn_y, int16_t btn_w, int16_t btn_h) {
return touch.is_inside(btn_x, btn_y, btn_x + btn_w, btn_y + btn_h);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
使用 CRTP 的可比较基类版本
如果我们有多个类需要比较功能,可以使用 CRTP 基类:
template<typename Derived, typename T>
class Comparable {
public:
// 三路比较
friend auto operator<=>(const Comparable&, const Comparable&) = default;
// 相等比较
friend bool operator==(const Comparable& a, const Comparable& b) {
return static_cast<const Derived&>(a).compare_impl(
static_cast<const Derived&>(b)
) == 0;
}
protected:
~Comparable() = default;
};
template<typename T>
class Point : public Comparable<Point<T>, T> {
T x_, y_;
public:
Point(T x, T y) : x_(x), y_(y) {}
int compare_impl(const Point& other) const {
if (x_ < other.x_) return -1;
if (x_ > other.x_) return 1;
if (y_ < other.y_) return -1;
if (y_ > other.y_) return 1;
return 0;
}
T x() const { return x_; }
T y() const { return y_; }
};
// 其他类也可以复用
template<typename T>
class Vector3D : public Comparable<Vector3D<T>, T> {
T x_, y_, z_;
public:
Vector3D(T x, T y, T z) : x_(x), y_(y), z_(z) {}
int compare_impl(const Vector3D& other) const {
if (auto cmp = x_ <=> other.x_; cmp != 0) return cmp < 0 ? -1 : 1;
if (auto cmp = y_ <=> other.y_; cmp != 0) return cmp < 0 ? -1 : 1;
if (auto cmp = z_ <=> other.z_; cmp != 0) return cmp < 0 ? -1 : 1;
return 0;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
嵌入式应用场景
场景1:传感器数据比较
template<typename T>
class SensorReading {
T value_;
uint32_t timestamp_;
public:
SensorReading(T value, uint32_t timestamp)
: value_(value), timestamp_(timestamp) {}
// 按值比较(用于阈值检测)
friend bool operator==(const SensorReading& a, const SensorReading& b) {
return a.value_ == b.value_;
}
friend auto operator<=>(const SensorReading& a, const SensorReading& b) {
return a.value_ <=> b.value_;
}
// 按时间戳比较(用于排序)
friend bool chronological_order(const SensorReading& a,
const SensorReading& b) {
return a.timestamp_ < b.timestamp_;
}
T value() const { return value_; }
uint32_t timestamp() const { return timestamp_; }
};
// 使用
SensorReading<int> temp1{25, 1000};
SensorReading<int> temp2{30, 1005};
if (temp2 > temp1) {
// 温度升高
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
场景2:寄存器地址比较
template<typename AddrType, typename DataType>
class Register {
AddrType address_;
DataType value_;
public:
constexpr Register(AddrType addr, DataType val)
: address_(addr), value_(val) {}
// 按地址比较(用于查找)
friend bool operator==(const Register& a, const Register& b) {
return a.address_ == b.address_;
}
friend auto operator<=>(const Register& a, const Register& b) {
return a.address_ <=> b.address_;
}
AddrType address() const { return address_; }
DataType value() const { return value_; }
};
// 使用
using GPIOReg = Register<uint32_t, uint32_t>;
constexpr GPIOReg gpio_a{0x40020000, 0};
constexpr GPIOReg gpio_b{0x40020400, 0};
if (gpio_a < gpio_b) {
// gpio_a 的地址更小
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
场景3:配置参数验证
template<typename T>
class ConfigParameter {
const char* name_;
T value_;
T min_;
T max_;
public:
constexpr ConfigParameter(const char* name, T val, T min_val, T max_val)
: name_(name), value_(val), min_(min_val), max_(max_val) {
// 编译期验证
static_assert(min_val <= max_val, "Invalid range");
}
// 按名称比较
friend bool operator==(const ConfigParameter& a, const ConfigParameter& b) {
return std::strcmp(a.name_, b.name_) == 0;
}
// 按值比较
friend bool operator<(const ConfigParameter& a, const ConfigParameter& b) {
return a.value_ < b.value_;
}
constexpr bool is_valid() const {
return value_ >= min_ && value_ <= max_;
}
const char* name() const { return name_; }
T value() const { return value_; }
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
场景4:通信协议数据包比较
template<typename SeqType, typename PayloadSize>
class Packet {
SeqType sequence_;
PayloadSize size_;
uint8_t data_[256];
public:
Packet(SeqType seq, PayloadSize sz) : sequence_(seq), size_(sz) {}
// 按序列号比较
friend auto operator<=>(const Packet& a, const Packet& b) {
return a.sequence_ <=> b.sequence_;
}
friend bool operator==(const Packet& a, const Packet& b) {
return a.sequence_ == b.sequence_;
}
SeqType sequence() const { return sequence_; }
PayloadSize size() const { return size_; }
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
常见陷阱与解决方案
陷阱1:友元函数不在全局作用域
template<typename T>
class Point {
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
Point<int> p1, p2;
// operator==(p1, p2); // ❌ 可能找不到(取决于编译器)
p1 == p2; // ✅ 通过ADL找到2
3
4
5
6
7
8
9
10
解决方案:始终使用 p1 == p2 的形式,不要直接调用 operator==。
陷阱2:模板参数推导失败
template<typename T>
class Point {
template<typename U>
friend Point<U> operator+(const Point<U>& a, const Point<U>& b);
};
template<typename U>
Point<U> operator+(const Point<U>& a, const Point<U>& b) {
return {a.x + b.x, a.y + b.y}; // ❌ 无法访问私有成员
}2
3
4
5
6
7
8
9
10
解决方案:在类内定义友元函数,或使用公共访问器。
陷阱3:无限递归的 CRTP
template<typename Derived>
class Base {
public:
void foo() {
static_cast<Derived*>(this)->foo(); // ❌ 无限递归!
}
};
class Derived : public Base<Derived> {
public:
void foo() {
// 这里会调用 Base::foo,形成无限循环
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
解决方案:确保 Derived::foo 和 Base::foo 有不同的名称,或者使用 this->foo() 而不是强转后调用。
陷阱4:返回局部变量的引用
template<typename T>
class Point {
friend const Point& operator+(const Point& a, const Point& b) {
Point result{a.x + b.x, a.y + b.y}; // ❌ 局部变量
return result; // ❌ 返回局部变量的引用!
}
};2
3
4
5
6
7
解决方案:返回值而非引用:
friend Point operator+(const Point& a, const Point& b) {
return {a.x + b.x, a.y + b.y}; // ✅ 返回值(可能被RVO优化)
}2
3
陷阱5:C++20 三路比较的默认实现
template<typename T>
class Point {
T x, y;
public:
// 默认的 operator<=> 会逐成员比较
friend auto operator<=>(const Point&, const Point&) = default;
};
Point<int*> p1, p2;
// p1 == p2; // ❌ 指针比较,不是值比较!2
3
4
5
6
7
8
9
10
解决方案:为指针类型自定义比较,或禁用指针类型的实例化。
template<std::integral T>
class Point { // 使用 Concept 约束
T x, y;
public:
friend auto operator<=>(const Point&, const Point&) = default;
};2
3
4
5
6
陷阱6:友元函数的模板参数推导
template<typename T>
class Point {
template<typename U>
friend bool operator==(const Point<U>&, const Point<U>&);
// ⚠️ 这会让 Point<int> 和 Point<double> 也能比较
// 但可能不是你想要的!
};2
3
4
5
6
7
解决方案:使用 std::same_as 约束或使用非模板友元。
// 方案1:非模板友元(推荐)
template<typename T>
class Point {
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
// 方案2:C++20 约束
template<typename T>
class Point {
template<typename U>
friend bool operator==(const Point<U>& a, const Point<U>& b)
requires std::same_as<U, T> {
return a.x == b.x && a.y == b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C++20 新特性:Spaceship 运算符
C++20 的三路比较运算符(operator<=>,俗称 Spaceship 运算符)彻底改变了比较运算符的写法。
默认生成
template<typename T>
class Point {
T x, y;
public:
// 一行代码,自动生成 ==、!=、<、<=、>、>=
friend auto operator<=>(const Point&, const Point&) = default;
};2
3
4
5
6
7
自定义实现
template<typename T>
class Point {
T x, y;
public:
friend auto operator<=>(const Point& a, const Point& b) {
if (auto cmp = a.x <=> b.x; cmp != 0) return cmp;
return a.y <=> b.y;
}
// 三路比较不会自动生成 ==,需要单独定义
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
比较类别
operator<=> 返回不同的比较类别:
| 返回类型 | 说明 | 示例类型 |
|---|---|---|
std::strong_ordering | 完全可替换 | int、std::string |
std::weak_ordering | 等价但可替换 | float(NaN) |
std::partial_ordering | 部分可比较 | 复数(只有相等才有意义) |
#include <compare>
template<typename T>
class Point {
T x, y;
public:
// 指定返回类型
friend std::strong_ordering operator<=>(const Point& a, const Point& b) {
if (auto cmp = a.x <=> b.x; cmp != 0) return cmp;
return a.y <=> b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
同类和异类比较
C++20 支持为同类和异类比较分别定义运算符:
template<typename T>
class Point {
T x, y;
public:
// 同类比较
friend auto operator<=>(const Point&, const Point&) = default;
// 异类比较(用默认的 rewritable 方式)
template<typename U>
friend auto operator<=>(const Point& a, const Point<U>& b) {
if (auto cmp = a.x <=> b.x; cmp != 0) return cmp;
return a.y <=> b.y;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
性能考量
编译期开销
友元注入和 Barton-Nackman Trick 的编译期开销主要来自:
- 模板实例化:每个类型组合都会生成新代码
- 符号表膨胀:大量友元函数会增加符号表
- ADL 查找:编译器需要额外进行 ADL 查找
运行时开销
正确使用下,零运行时开销:
// 编译前
Point<int> p1{1, 2}, p2{3, 4};
bool result = (p1 == p2);
// 编译后(近似)
bool result = (p1.x == p2.x && p1.y == p2.y);
// 完全内联,无函数调用2
3
4
5
6
7
优化建议
- 对于小型类:使用友元注入,代码简洁
- 对于大型层次结构:使用 CRTP 基类复用代码
- 对于 C++20:优先使用
operator<=>默认实现 - 限制实例化:使用 Concepts 约束模板参数
// ✅ 好:使用 Concepts 限制
template<std::integral T>
class Point { /* ... */ };
// ❌ 不好:对任何类型都实例化
template<typename T>
class Point { /* ... */ };2
3
4
5
6
7
- 使用
constexpr:鼓励编译期计算
constexpr Point<int> p1{1, 2};
constexpr Point<int> p2{3, 4};
constexpr bool eq = (p1 == p2); // 编译期计算
static_assert(!eq);2
3
4
小结
本章我们深入探讨了模板友元与 Barton-Nackman Trick:
核心概念
| 概念 | 作用 | 使用场景 |
|---|---|---|
| 友元注入 | 在类内定义友元函数,通过ADL可在外部调用 | 简化运算符重载 |
| Barton-Nackman Trick | 约束运算符只在特定类型下可用 | 早期约束泛型编程 |
| CRTP | 派生类作为基类的模板参数 | 共享基类逻辑 |
| 三路比较 | C++20统一比较运算符 | 简化比较运算符定义 |
实战要点
运算符重载选择:
==、!=、<、<=、>、>=:友元函数+=、-=、*=、/=:成员函数- C++20:使用
operator<=>
嵌入式优化:
- 使用
constexpr编译期计算 - 避免浮点运算,使用整数距离平方
- 使用 Concepts 约束减少实例化
- 使用
常见陷阱:
- 友元函数只能通过 ADL 找到
- 返回值而非引用
- CRTP 避免无限递归
现代C++推荐:
- 简单场景:直接友元注入
- 复杂场景:CRTP 基类
- C++20:
operator<=>默认实现 + Concepts
下一章,我们将探讨模板元编程进阶,学习 SFINAE、类型萃取、标签分发等高级技巧,并实现一个编译期反射系统。