PrevNext

代码模板

代码模板是预先假定会出现在每个文件中的一段代码。你可以放心编写自己的模板,也可以完全不使用模板!下面给出一个模板示例。

Warning!

USACO 规则禁止使用包括模板在内的预先编写代码。因此,请确保比赛时能够重新敲出自己的模板(或者干脆不使用模板)!

与其他竞赛语言相比,C++ 代码模板可以利用更强大的功能(例如宏),也更容易按每位选手的需要进行定制。

Code Snippet: C++ Short Template (Click to expand)
int main() { setIO(); }

本模块将介绍上述代码使用的许多功能。

什么是泛型代码?

Resources
Aryansh

相关内容已整合到本模块中。

LCPP

泛型代码是信息学中的重要概念。当然,你也可以避开它,继续用高度特化的方式编写代码。因此,不妨先思考使用泛型代码的目的。

泛型代码具有很强的适应性,无需大幅修改就能立即用于多种场景。它可以被复用、扩展,甚至进行版本管理,从而有效节省时间。在信息学中,时间至关重要,这既指算法的时间_复杂度_,也指_编写代码_所需的时间。

即使正在编写非常具体的数据结构或算法,要真正掌握它,也值得思考:“能否把所学内容推广到更广泛的一类问题?”随后尝试实际进行泛化。不过,在继续之前,请注意下面的警告:

Warning!

泛型代码很容易助长黑盒化:你写下一段代码后,要么忘记它的含义,要么从一开始就不知道它究竟做了什么。这是一个常见陷阱;如果真想掌握信息学概念,就应尽量避免这种情况。

现代 C++ 在当今信息学领域占有重要地位,它提供了多种内置功能来支持并简化泛型代码。这里将介绍几项重要的基础功能,它们很值得加入你的编程工具箱。

是可扩展代码中最重要的工具。如果想编写一种数据结构,并通过多个成员函数处理其中保存的数据,类就非常合适。类可以包含公有和私有部分。例如,考虑一个 Human 类,其中维护了几个相关的成员函数。

#include <bits/stdc++.h>
using namespace std;
class Human {
private: // internal properties
int body_temp;
int temper;
string name;
public: // external reactions

那么,怎样实际使用(以及复用)刚刚创建的类数据结构呢?我们需要创建该类的_实例_,这种实例具体称为对象

下面创建了一个名为 salHuman 类实例。

Human sal;

我们当然希望用基本属性(上面的 body_temperaturetemper)初始化 Human 类的每个对象,但问题是无法直接访问这些私有属性,因此它们仍未初始化。

一种部分解决办法,是直接在类声明中初始化变量:

#include <bits/stdc++.h>
using namespace std;
class Human {
private: // internal properties
int 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_temperaturetemper,从而在初始化时控制 Sal 的内部属性。完整代码如下:

#include <bits/stdc++.h>
using namespace std;
class Human {
private: // internal properties
int body_temp;
int temper;
string name;
public: // external reactions

如果想创建多个 Human 实例,并为它们分别设置初始属性,这种写法很容易扩展。还可以进一步泛化:创建一个外部 condition 函数来输出感受和情绪,而无需反复编写相同逻辑。

// Prints out the condition of a human to cout
void 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 people
condition(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 Sal
Human sal("Sal", 98, 25);
condition(sal); // Get Sal's initial condition
sal.name = "Sally"; // Sal's friends sometimes call him Sally
sal.body_temp = 102; // Sal gets sick
sal.temper = 40; // He develops a bad temper due to his sickness
condition(sal); // Now we get Sal's new condition
}

最后,还可以完全去掉构造函数,改用按照内部变量_声明顺序_排列的初始化列表。在 Human 中,变量依次为 body_temptempername,因此可以移除构造函数并使用如下初始化列表:

human Sal{98, 25, "Sal"};

这些方法让类与结构体的初始化和操作都十分简洁,而这正是泛型代码的重要组成部分。

模板

Human 类示例虽然定义完整,但主要是为了展示类和结构体的灵活性。现在换一个更简单的例子。下面的结构体表示空间中的三维点,并创建了两个这样的点:p1p2

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(); }

vvector<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 定义类型别名

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 std
using 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>;
Warning!

如果使用过多类型别名或宏,你的代码将变得只有自己能看懂!

最后,还可以把 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 main
using U = Point::T;
}

本节最后介绍用于定义#define

Resources
CPH

宏的简单示例,并介绍一个常见错误

GCC

参考资料

GFG
LCPP

#define 本质上是在编译前(预处理阶段)执行的简单查找替换。因此它很容易使用:#define NAME VALUE 会查找代码中所有 NAME,并将其替换为 VALUE

下面的示例通过查找替换,将 MOD 定义为 1e9 + 7

#define MOD 1e9 + 7
int main() {
cout << int(MOD) << "\n"; // outputs 1000000007
cout << int(MOD * 2) << "\n"; // outputs 1000000014
cout << int(2 * MOD) << "\n"; // outputs 2000000007
}

但这样做显然并不理想。更好的替代方案如下:

const int MOD = 1e9 + 7;
int main() { cout << MOD << "\n"; }

此外,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

反复输入 firstsecond 可能很麻烦,尤其是在使用嵌套二元组时。这些宏可以解决该问题。

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 3D
template <class T> struct Point {
#if (d2)
T x;
T y;
#else
T x;
T y;
T z;
#endif
};

如果不排斥 #define,还可以用 #ifdef#ifndef 检查某个宏是否已通过 #define 定义。

// #define 2D // Uncomment to make Point 2D
template <class T> struct Point {
#ifdef 2D
T x;
T y;
#else
T 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 v1
inline namespace v2 {
// removed the buggy_feature from this new version
const string buggy_feature = "what buggy feature?";
const string greeting = "hi";
} // namespace v2
int main() {
cout << buggy_feature << "\n"; // outputs "what buggy feature?"
cout << v1::buggy_feature << "\n"; // outputs "bugs"
}

现在,不指定命名空间时会自动使用 v2;如果想访问 v1 中的 buggy_feature,只需写 v1::buggy_feature。这样就实现了简单而有效的版本控制!

Module Progress:

PrevNext