2.7 回顾序言中的成绩程序
经过一段时间的学习,不妨回望一下 序言中的学生成绩程序,读取学生成绩、检查成绩是否合法、筛选不及格学生、输出结果,看看我们有什么长进。
明确职责
Score保证成绩始终在0到100之间,是不变式;Student以结构体组合学号、姓名和成绩。
这里没有需要运行时替换的学生类型,因此不需要虚函数或继承树。Student 以结构体存在很合适。
成绩类
#include <optional>
class Score {
public:
[[nodiscard]] static std::optional<Score> from(int value) {
if (value < 0 || value > 100)
return std::nullopt;
return Score{value};
}
[[nodiscard]] int value() const noexcept { return value_; }
[[nodiscard]] bool passed(int threshold = 60) const noexcept {
return value_ >= threshold;
}
private:
explicit Score(int value) : value_{value} {}
int value_;
};Score 类的构造函数是 private 的,并加上了 explicit 防止隐式转换。这意味着创建新的分数只能通过静态的 Score::from 方法实现。
auto foo = Score::from(80);
Score bar{80};
// 'Score::Score(int)' is private within this context这是因为分数越界是一个可能发生的失败。这样做不允许调用者构造非法的 Score,否则一旦调用者拿到 Score,后续代码就可以依赖它的不变式。而 Score::from 返回 std::optional<Score>,在遇到非法值时返回 std::nullopt,避免构造非法值。
Score 只保存一个整数,没有管理动态内存或者其他资源,不需要手写析构、复制或移动,编译器会自动管理好行为 —— Rule of Zero。
学生信息结构体
struct Student {
int id{};
std::string name;
Score score;
};Student 使用值语义。std::string 会自己管理文本资源,因此 Student 不需要手写析构、复制或移动操作 —— Rule of Zero。
读入学生信息的函数
std::optional<Student> read_student(std::istream &input) {
int id{};
std::string name;
int raw_score{};
if (!(input >> id >> name >> raw_score))
return std::nullopt;
const auto score = Score::from(raw_score);
if (!score)
return std::nullopt;
return Student{id, std::move(name), *score};
}read_student 返回 std::optional<Student>。返回值优先,成功时返回一个完整的 Student,失败时返回空值,十分清晰。在返回时,编译器会自动进行复制消除等优化,无需手动 std::move。
主函数
int main() {
int count{};
std::cout << "Number of students: ";
if (!(std::cin >> count) || count < 0) {
std::cerr << "Invalid student count\n";
return 1;
}
std::vector<Student> students;
students.reserve(static_cast<std::size_t>(count));
std::cout << "Enter id, name and score for each student:\n";
for (int index{}; index < count; ++index) {
auto student = read_student(std::cin);
if (!student) {
std::cerr << "Invalid student data\n";
return 1;
}
students.push_back(std::move(*student));
}
auto failed_students =
students | std::views::filter([](const Student &student) {
return !student.score.passed();
});
std::cout << "Students who failed:\n";
for (const Student &student : failed_students) {
std::cout << student.id << ' ' << student.name << ' '
<< student.score.value() << '\n';
}
}在输入人数后,使用 students.reserve 预留空间。在输入循环中,students.push_back(std::move(*student)) 中使用了 std::move 以使用移动语义。
std::views::filter 创建一个借用 students 的视图。视图只在后面的 for 中使用,此时 students 仍然存活,因此生命周期是安全的。
小结
完整代码
#include <cstddef>
#include <iostream>
#include <optional>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
class Score {
public:
[[nodiscard]] static std::optional<Score> from(int value) {
if (value < 0 || value > 100)
return std::nullopt;
return Score{value};
}
[[nodiscard]] int value() const noexcept { return value_; }
[[nodiscard]] bool passed(int threshold = 60) const noexcept {
return value_ >= threshold;
}
private:
explicit Score(int value) : value_{value} {}
int value_;
};
struct Student {
int id{};
std::string name;
Score score;
};
std::optional<Student> read_student(std::istream &input) {
int id{};
std::string name;
int raw_score{};
if (!(input >> id >> name >> raw_score))
return std::nullopt;
const auto score = Score::from(raw_score);
if (!score)
return std::nullopt;
return Student{id, std::move(name), *score};
}
int main() {
int count{};
std::cout << "Number of students: ";
if (!(std::cin >> count) || count < 0) {
std::cerr << "Invalid student count\n";
return 1;
}
std::vector<Student> students;
students.reserve(static_cast<std::size_t>(count));
std::cout << "Enter id, name and score for each student:\n";
for (int index{}; index < count; ++index) {
auto student = read_student(std::cin);
if (!student) {
std::cerr << "Invalid student data\n";
return 1;
}
students.push_back(std::move(*student));
}
auto failed_students =
students | std::views::filter([](const Student &student) {
return !student.score.passed();
});
std::cout << "Students who failed:\n";
for (const Student &student : failed_students) {
std::cout << student.id << ' ' << student.name << ' '
<< student.score.value() << '\n';
}
}- 先按职责拆分问题,再决定哪些部分需要成为类
Score用类维护不变式,Student用结构体表达数据- 标准库容器和
optional让资源管理遵循 Rule of Zero optional、范围视图和返回值让控制流直接体现在类型与代码中