代码模板
代码模板是预先假定会出现在每个文件中的一段代码。你可以放心编写自己的模板,也可以完全不使用模板!下面给出一个模板示例。
USACO 规则禁止使用包括模板在内的预先编写代码。因此,请确保比赛时能够重新敲出自己的模板(或者干脆不使用模板)!
与其他竞赛语言相比,C++ 代码模板可以利用更强大的功能(例如宏),也更容易按每位选手的需要进行定制。
| Resources | |||||
|---|---|---|---|---|---|
| AryanshS | |||||
| Benq | |||||
Code Snippet: C++ Short Template (Click to expand)int main() { setIO(); }
本模块将介绍上述代码使用的许多功能。
什么是泛型代码?
| Resources | |||||
|---|---|---|---|---|---|
| Aryansh | 相关内容已整合到本模块中。 | ||||
| LCPP | |||||
泛型代码是信息学中的重要概念。当然,你也可以避开它,继续用高度特化的方式编写代码。因此,不妨先思考使用泛型代码的目的。
泛型代码具有很强的适应性,无需大幅修改就能立即用于多种场景。它可以被复用、扩展,甚至进行版本管理,从而有效节省时间。在信息学中,时间至关重要,这既指算法的时间_复杂度_,也指_编写代码_所需的时间。
即使正在编写非常具体的数据结构或算法,要真正掌握它,也值得思考:“能否把所学内容推广到更广泛的一类问题?”随后尝试实际进行泛化。不过,在继续之前,请注意下面的警告:
泛型代码很容易助长黑盒化:你写下一段代码后,要么忘记它的含义,要么从一开始就不知道它究竟做了什么。这是一个常见陷阱;如果真想掌握信息学概念,就应尽量避免这种情况。
现代 C++ 在当今信息学领域占有重要地位,它提供了多种内置功能来支持并简化泛型代码。这里将介绍几项重要的基础功能,它们很值得加入你的编程工具箱。
类
类是可扩展代码中最重要的工具。如果想编写一种数据结构,并通过多个成员函数处理其中保存的数据,类就非常合适。类可以包含公有和私有部分。例如,考虑一个 Human 类,其中维护了几个相关的成员函数。
#include <bits/stdc++.h>using namespace std;class Human {private: // internal propertiesint body_temp;int temper;string name;public: // external reactions
那么,怎样实际使用(以及复用)刚刚创建的类数据结构呢?我们需要创建该类的_实例_,这种实例具体称为对象。
下面创建了一个名为 sal 的 Human 类实例。
Human sal;
我们当然希望用基本属性(上面的 body_temperature 和 temper)初始化 Human 类的每个对象,但问题是无法直接访问这些私有属性,因此它们仍未初始化。
一种部分解决办法,是直接在类声明中初始化变量:
#include <bits/stdc++.h>using namespace std;class Human {private: // internal propertiesint body_temp = 98;int temper = 25;string name = "Sal";public: // external reactions
这样就有了可用的初始值。现在可以在 main() 中创建 sal,并调用它从 Human 类实例化得到的成员函数。
int main() {Human sal;cout << sal.get_name() << " feels " << sal.get_feeling() << " and is "<< sal.get_emotion() << endl;}
要彻底解决问题,我们可以把这些变量改为公有,但更巧妙的做法是为该类编写构造函数。本质上,每当创建类实例时,构造函数都会自动调用。当我们希望变量创建后不可被随意修改,却又需要在使用前初始化它们时,构造函数非常有用。
可以为 Human 类创建构造函数,要求初始化 body_temperature 和 temper,从而在初始化时控制 Sal 的内部属性。完整代码如下:
#include <bits/stdc++.h>using namespace std;class Human {private: // internal propertiesint body_temp;int temper;string name;public: // external reactions
如果想创建多个 Human 实例,并为它们分别设置初始属性,这种写法很容易扩展。还可以进一步泛化:创建一个外部 condition 函数来输出感受和情绪,而无需反复编写相同逻辑。
// Prints out the condition of a human to coutvoid condition(Human h) {cout << h.get_name() << " feels " << h.get_feeling() << " and is "<< h.get_emotion() << endl;}int main() {Human sal("Sal", 98, 25);Human bob("Bob", 100, 9);Human joe("Joe", 85, 35);// Print out the conditions of all three peoplecondition(sal);condition(bob);condition(joe);}
关于构造函数还有一个具体而实用的技巧:如果只是初始化属性,可以采用另一种声明方式,它的执行速度明显更快;此时也可以让参数与变量同名,而且这是有明确定义的行为:
Human(string name, int body_temp, int temper): name(name), body_temp(body_temp), temper(temper) {}
结构体
如果并不在意保持属性私有,而更需要一种通用、可复用的数据结构,结构体会很有用。结构体中的所有内容默认都是公有的。采用上面改写后的构造函数,Human 结构体如下:
struct Human {int body_temp;int temper;string name;Human(string name_, int body_temp_, int temper_) {name = name_;body_temp = body_temp_;temper = temper_;}
由于结构体较为开放,它会更易于管理,也能与周围代码更紧密地配合,从而进行如下操作:
int main() {// Initialize SalHuman sal("Sal", 98, 25);condition(sal); // Get Sal's initial conditionsal.name = "Sally"; // Sal's friends sometimes call him Sallysal.body_temp = 102; // Sal gets sicksal.temper = 40; // He develops a bad temper due to his sicknesscondition(sal); // Now we get Sal's new condition}
最后,还可以完全去掉构造函数,改用按照内部变量_声明顺序_排列的初始化列表。在 Human 中,变量依次为 body_temp、temper 和 name,因此可以移除构造函数并使用如下初始化列表:
human Sal{98, 25, "Sal"};
这些方法让类与结构体的初始化和操作都十分简洁,而这正是泛型代码的重要组成部分。
模板
| Resources | |||||
|---|---|---|---|---|---|
| LCPP | |||||
| LCPP | |||||
Human 类示例虽然定义完整,但主要是为了展示类和结构体的灵活性。现在换一个更简单的例子。下面的结构体表示空间中的三维点,并创建了两个这样的点:p1 和 p2。
struct Point3D {int x;int y;int z;} p1{1, 2, 3}, p2{3, 4, 5}; // We can make some instances right before the ;
如果想创建一个坐标为 double 的点 p3 呢?这时就不得不再创建一个点结构体:
struct Point3D {int x;int y;int z;} p1{1, 2, 3}, p2{3, 4, 5};struct Point3DDouble {double x;double y;double z;} p3{1.1, 2.2, 3.3};
乍看之下似乎不算太糟,但如果每次类型变化都要反复创建这样的结构体,就会非常繁琐。我们需要更好的办法。
这时模板就能派上用场。可以用 template<...> 指定模板条件,然后照常定义结构体。具体来说,使用类型 T 后可以写成:
template <class T> struct Point3D {T x;T y;T z;};Point3D<int> p1{1, 2, 3};Point3D<int> p2{3, 4, 5};Point3D<double> p3{1.1, 2.2, 3.3};Point3D<long long> p4{9223372036854775807, 9223372036854775807, 9223372036854775807};
模板绝不局限于类和结构体,还可用于函数等许多场景。例如,看看下面的 ckmin 函数:
/*** If b is less than a, this changes the value of a to that of b* and returns true. If not, the function simply returns false.*/template <class T> bool ckmin(T &a, const T &b) {if (b < a) {a = b;return true;}return false;}
一个有趣的应用是获取各种容器的大小。容器的 size 成员函数通常返回与 int 不兼容的类型,但可以轻松编写模板函数来处理,从而一次适配所有容器类型:
template <class T> int sz(const T &container) { return (int)container.size(); }
当 v 是 vector<int> 时,可以通过 sz<vector<int>>(v) 调用该函数。不过从 C++11 开始,函数能够推导模板参数(类和结构体则要到 C++14、C++17 才逐步支持),因此只需写 sz(v)!
如果想在模板中放入多个参数,以处理多种类型呢?以下面的二元组次关键字比较器为例:
struct CPS {template <class T, class U>bool operator()(const pair<T, U> &a, const pair<T, U> &b) {return make_pair(a.second, a.first) < make_pair(b.second, b.first);}};
在这种设计中,两个模板参数都只用于一个函数,因此可以轻松推导。例如,在 C++11 中声明一个 pair<double, int> 的集合,只需写 set<pair<double, int>, CPS>。
模板中还能放入哪些类型?粗略来说,在 C++17 之前,模板参数只接受类类型和基本类型;在许多情况下,函数只能直接推导类类型。
模板甚至可以接受数量可变的参数,这称为可变参数模板;也可以在模板中嵌套模板。两者都超出本基础介绍的范围,可分别参阅这里和这里。
顺带一提,模板让我们能够充分控制代码的通用程度,而 C++ 标准库本身也有大量内容在底层通过模板实现。
使用 using 定义类型别名
| Resources | |||||
|---|---|---|---|---|---|
| LCPP | |||||
| Quora | |||||
| CPPR |
| ||||
typedef 如今已经相当过时(尽管仍有人使用),它大致可以看作语义更别扭、用法更麻烦的 using,因此这里不作介绍。
using 是一个非常实用的关键字,经常用于简化命名空间前缀。例如:
using namespace std;
实际上允许使用整个命名空间。当然,using namespace std 通常只在竞赛编程中使用,在其他场景往往不受推荐;我们可以用 using 做更精细的简化。
假设代码中大量使用 std::cout,每次输入都很麻烦,可以写:
using std::cout;
此后只需使用 cout。如果使用字符串时既不想写 std::string,也不想写 string,可以连续使用两次 using:
using std::string; // Unnecessary if already using namespace stdusing str = string; // Use str as an alias for string
也可以压缩为一条语句:
using str = std::string; // str is an alias for std::string directly
在竞赛编程这类强调速度的环境中,可以创建更多别名,甚至让别名引用其他别名(见 ll):
using namespace std;using ll = long long;using str = string;using pii = pair<int, int>;using pll = pair<ll, ll>;using vi = vector<int>;
如果使用过多类型别名或宏,你的代码将变得只有自己能看懂!
最后,还可以把 using 与模板结合起来!例如,如果希望用 arr<int, 6> 代替 std::array<int, 6>,可以写:
template <class T, int SZ> using arr = std::array<T, SZ>;
再例如,如果希望用 ai 表示整数数组,也可以让 ai<6> 这样的写法生效:
template <int SZ> using ai = std::array<int, SZ>;
using 声明具有严格的作用域限制,这一点很重要:它不会在定义它的作用域外生效。如果要让声明在整个程序中可用,就必须在全局作用域中使用它。不过,如果只想创建可复用结构体,并在内部指定类型,也可以这样写:
struct Point {using T = int;/** Within this scope, T is an alias for int.* Just change this declaration to change T's meaning within this struct*/T x;T y;T z;};
如果希望在 Point 外部访问类型别名 T 的实际含义,也很容易做到:
struct Point {using T = int;T x;T y;T z;};int main() {// U becomes a copy of T from Point's scope and is now in the scope of mainusing U = Point::T;}
宏
本节最后介绍用于定义宏的 #define。
| Resources | |||||
|---|---|---|---|---|---|
| CPH | 宏的简单示例,并介绍一个常见错误 | ||||
| GCC | 参考资料 | ||||
| GFG | |||||
| LCPP | |||||
#define 本质上是在编译前(预处理阶段)执行的简单查找替换。因此它很容易使用:#define NAME VALUE 会查找代码中所有 NAME,并将其替换为 VALUE。
下面的示例通过查找替换,将 MOD 定义为 1e9 + 7。
#define MOD 1e9 + 7int main() {cout << int(MOD) << "\n"; // outputs 1000000007cout << int(MOD * 2) << "\n"; // outputs 1000000014cout << int(2 * MOD) << "\n"; // outputs 2000000007}
但这样做显然并不理想。更好的替代方案如下:
const int MOD = 1e9 + 7;int main() { cout << MOD << "\n"; }
| Resources | |||||
|---|---|---|---|---|---|
| LCPP | |||||
此外,using 通常优于 #define。例如,下面使用 #define 的代码无法编译(改用 using 就可以)。
#define ll long long// using ll = long long;int main() { cout << ll(1e18); }
因此,应尽可能避免使用 #define。当然,也有一些竞赛程序员大量使用宏,下面给出几个示例。
二元组
using pi = pair<int, int>;#define mp make_pair#define f first#define s second
反复输入 first 和 second 可能很麻烦,尤其是在使用嵌套二元组时。这些宏可以解决该问题。
vector
using vi = vector<int>;#define sz(x) int((x).size())#define all(x) begin(x), end(x)
我们将大小转换为有符号整数,以避免无符号整数下溢,如下例所示:
vi x;cout << x.size() - 1 << "\n"; // otutputs 18446744073709551615 (incorrect)cout << sz(x) - 1 << "\n"; // outputs -1 (correct)
all(v) 可以让排序 vector 的部分或全部元素时写得更简短。
vi v{2, 4, 1, 5, 3};sort(1 + all(v)); // v is now {2, 1, 3, 4, 5}// This expands to sort(1 + begin(v), end(v));sort(all(v)); // {1, 2, 3, 4, 5}
预处理逻辑
是否想过让程序根据某些初始条件以不同方式编译?可以使用 #if、#else、#ifdef 和 #ifndef 等预处理指令来实现。
例如,有时希望 Point 结构体表示二维点,有时又表示三维点,可以这样写:
const bool d2 = false; // true for 2D, false for 3Dtemplate <class T> struct Point {#if (d2)T x;T y;#elseT x;T y;T z;#endif};
如果不排斥 #define,还可以用 #ifdef 和 #ifndef 检查某个宏是否已通过 #define 定义。
// #define 2D // Uncomment to make Point 2Dtemplate <class T> struct Point {#ifdef 2DT x;T y;#elseT x;T y;T z;#endif}
这种机制有许多巧妙用途,包括版本管理。特别是,如果希望代码在不同 C++ 版本下采用不同行为,可以写:
#if (__cplusplus < 201703L)/*** "Clamps" v between the values of lo and hi if it's* out of the bounds defined by those two values.*/template <class T> constexpr const T &clamp(const T &v, const T &lo, const T &hi) {assert(lo <= hi);if (v < lo) {return lo;} else if (hi < v) {return hi;}return v;}#endif
命名空间
最后,可以编写自己的命名空间来隔离不同函数。命名空间中可以包含函数、变量、类,甚至更多命名空间;随后可以通过 using 声明使用整个命名空间。
namespace test {const string greeting = "hi";namespace test1 {const int time = 2;}using namespace test1;template <class T> struct TestDS {T s;void add(T x) { s += x; }T get() { return s; }};} // namespace test
这些内容相当直观。C++11 中还有一种较少见的功能:inline namespace。严格来说,内联命名空间并不是普通命名空间,但它允许我们分块组织代码,又不必为了访问内容而显式指定命名空间。
inline namespace test {const string greeting = "hi";}
那么,为什么需要内联命名空间?它可以实现一些有趣的功能。例如,假设程序旧版本 v1 中有某项功能,但在新版本 v2 中已经移除。
namespace v1 {const string buggy_feature = "bugs";const string greeting = "hi";} // namespace v1inline namespace v2 {// removed the buggy_feature from this new versionconst string buggy_feature = "what buggy feature?";const string greeting = "hi";} // namespace v2int main() {cout << buggy_feature << "\n"; // outputs "what buggy feature?"cout << v1::buggy_feature << "\n"; // outputs "bugs"}
现在,不指定命名空间时会自动使用 v2;如果想访问 v1 中的 buggy_feature,只需写 v1::buggy_feature。这样就实现了简单而有效的版本控制!