01
实现管道运算符
02
实现自定义字面量 _f
03
实现 print
以及特化 std::formatter
04
给定类模板修改,让其对每一个不同类型实例化有不同 ID
05
实现 scope_guard
类型
06
解释 std::atomic
初始化
07
throw new MyException
08
定义array
推导指引
09
名字查找的问题
10
遍历任意类数据成员
C++17
写法C++20
写法11
emplace_back()
的问题
12
实现make_vector()
13
关于 return std::move
14
以特殊方法修改命名空间中声明的对象
15
表达式模板
16
制造传递函数模板的宏
卢瑟们的作业展示。
提交 PR 不应当更改当前 README
,请将作业提交到 src群友提交
中,比如你要提交第一个作业:
你应当在 src群友提交第01题
中创建一个自己的 .md
或 .cpp
文件,文件名以自己交流群 ID 命名(或 GitHub 用户名都可,方便找到本人即可)。
答题的一般要求如下(题目额外要求也自行注意看):
main
函数,不得使其不运行(意思别捞偏门)。01
实现管道运算符日期:2023/7/21
出题人:mq白
给出代码:
int main(){
std::vector v{1, 2, 3};
std::function f {[](const int& i) {std::cout << i << ' '; } };
auto f2 = [](int& i) {i *= i; };
v | f2 | f;
}
1 4 9
答题者:andyli
#include <algorithm>
#include <vector>
#include <functional>
#include <iostream>
template <typename R, typename F>
auto operator|(R&& r, F&& f) {
for (auto&& x: r)
f(x);
return r;
}
int main() {
std::vector v{1, 2, 3};
std::function f{[](const int& i) { std::cout << i << ' '; }};
auto f2 = [](int& i) { i *= i; };
v | f2 | f;
}
很常规,没啥问题。
答题者:mq松鼠
#include <iostream>
#include <vector>
#include <functional>
auto operator | (std::vector<int>&& v,std::function<void(const int&)> f){
for(auto&i:v){
f(i);
}
return v;
}
auto operator | (std::vector<int>& v,std::function<void(int&)> f){
for(auto&i:v){
f(i);
}
return v;
}
int main(){
std::vector v{1, 2, 3};
std::function f {[](const int& i) {std::cout << i << 'n'; } };
auto f2 = [](int& i) {i *= i; };
v | f2 | f;
}
评价:闲的没事多写个重载,裱起来。
template<typename U, typename F>
requires std::regular_invocable<F, U&>//可加可不加,不会就不加
std::vector<U>& operator|(std::vector<U>& v1, F f) {
for (auto& i : v1) {
f(i);
}
return v1;
}
不使用模板:
std::vector<int>& operator|(std::vector<int>& v1, const std::function<void(int&)>& f) {
for (auto& i : v1) {
f(i);
}
return v1;
}
不使用范围 for
,使用 C++20 简写函数模板:
std::vector<int>& operator|(auto& v1, const auto& f) {
std::ranges::for_each(v1, f);
return v1;
}
各种其他答案的范式无非就是这些改来改去了,没必要再写。
很明显我们需要重载管道运算符 |,根据我们的调用形式 v | f2 | f
, 这种链式的调用,以及根据给出运行结果,我们可以知道,重载函数应当返回 v 的引用,并且 v 会被修改。
v | f2
调用 operator |
,operator | 中使用 f2 遍历了 v 中的每一个元素,然后返回 v 的引用,再 | f。
template<typename U, typename F>
requires std::regular_invocable<F, U&> //我们可以认为对模板形参U,F满足std::regular_invocable的约束
如果没接触过约束表达式,没关系,下面将简要的介绍。
requires 表达式如同一个返回 bool 的函数,而 U 和 F 作为类型填入 std::regular_invocable 的实参列表里,只要作为类型的 U、F 满足该表达式则返回 true;不满足则返回 false,称为“不满足约束”。不满足约束的类型自然不会执行后续的代码。
而 std::regular_invocable 我们可以简单看成对类型 U 的每一个值,我们是否可以调用函数 F,即调用 std::invoke
。
这就相当于我们在编译期对运行期做了想象,想象是否可以对 U 在运行期执行 F。如果可以那满足约束。
而函数主体则极为简单
std::vector<U>& operator|(std::vector<U>& v1, const F f) {
for (auto& i : v1) {
f(i);
}
return v1;
}
其中范围表达式 for (auto& i : v1)
,如同 for(auto i=v.begin();i!=v.end();++i){f(*i)}
:我们对 vector(范围)中的每一个元素应用一次 f 函数。返回时照常返回 v1。
如若不使用模板,则我们的形参列表得用 std::function 来接住我们使用的函数:
对范围中的每个成员应用 f 不需要返回值且需要对范围中的元素进行修改,所以第二个形参为 std::function<void(int&)>
。并且我们不需要对传进来的函数 f 进行修改与拷贝,所以加上 const 限定是个好习惯。
同样的我们可以不使用范围 for 而是更简单的 std::ranges::for_each(v1, f);
即同上一样对范围 v1内的每个元素,应用一次函数 f。
对于使用模板的形式,我们可以使用 C++20 的简写函数模板;简而言之,在函数形参列表中 auto 占位符会为模板形参列表追加一个虚设的模板形参。最开始的模板形式可以写成
std::vector<int>& operator|(auto& v1, const auto& f)
它和原形式相同。
02
实现自定义字面量 _f
日期:2023/7/22
出题人:mq白
给出代码:
int main(){
std::cout << "乐 :{} *n"_f(5);
std::cout << "乐 :{0} {0} *n"_f(5);
std::cout << "乐 :{:b} *n"_f(0b01010101);
std::cout << "{:*<10}"_f("卢瑟");
std::cout << 'n';
int n{};
std::cin >> n;
std::cout << "π:{:.{}f}n"_f(std::numbers::pi_v<double>, n);
}
乐 :5 *
乐 :5 5 *
乐 :1010101 *
卢瑟******
6
π:3.141593
6
为输入,决定
答题者:andyli
#include <format>
#include <iostream>
#include <string_view>
#include <string>
namespace impl {
struct Helper {
const std::string_view s;
Helper(const char* s, std::size_t len): s(s, len) {}
template <typename... Args>
std::string operator()(Args&&... args) const {
return std::vformat(s, std::make_format_args(args...));
}
};
} // namespace impl
impl::Helper operator""_f(const char* s, std::size_t len) noexcept {
return {s, len};
}
int main() {
std::cout << "乐 :{} *n"_f(5);
std::cout << "乐 :{0} {0} *n"_f(5);
std::cout << "乐 :{:b} *n"_f(0b01010101);
std::cout << "{:*<10}"_f("卢瑟");
std::cout << 'n';
int n{};
std::cin >> n;
std::cout << "π:{:.{}f}n"_f(std::numbers::pi_v<double>, n);
}
constexpr auto operator""_f(const char* fmt, size_t) {
return[=]<typename... T>(T&&... Args) { return std::vformat(fmt, std::make_format_args(Args...)); };
}
我们需要使用到 C++11 用户定义字面量,""_f
正是用户自定义字面量。
但字面量运算符(用户定义字面量所调用的函数被称为字面量运算符)的形参列表有一些限制,我们需要的是 const char *, std::size_t
这样的形参列表,恰好这是允许的;而字面量运算符的返回类型需要自定义,这个类型需要在内部重载 operator()
,以满足上述字面量像函数一样调用的要求。
我们一步一步来:
void operator""_test(const char* str, std::size_t){
std::cout << str << 'n';
}
"luse"_test; //调用了字面量运算符,打印 luse
std::size_t operator""_test(const char* , std::size_t len){
return len;
}
std::size_t len = "luse"_test; //调用了字面量运算符,返回 luse 的长度 4
上面这段代码的两个使用示例展示了我们用户定义字面量的基本使用,尤其注意第二段,返回值。如果要做到像 "xxx"_f(xxx)
这样调用,就得在返回类型上做点手脚。
struct X{
std::size_t operator()(std::size_t n)const{
return n;
}
};
X operator""_test(const char* , std::size_t){
return {};
}
std::cout<<"无意义"_test(1); //打印 1
以上这段简单的代码很好的完成了我们需要的调用形式,那么是时候完成题目要求的功能了。最简单的方式是直接使用 C++20 format 库进行格式化。
namespace impl {
struct Helper {
const std::string_view s;
Helper(const char* s, std::size_t len): s(s, len) {}
template <typename... Args>
std::string operator()(Args&&... args) const {
return std::vformat(s, std::make_format_args(args...));
}
};
} // namespace impl
impl::Helper operator""_f(const char* s, std::size_t len) noexcept {
return {s, len};
}
operator""_f
本身非常简单,只是用来把传入的参数(格式字符串)和长度,构造 impl::Helper
对象再返回。Helper
类型使用了一个 string_view
作为数据成员,存储了格式字符串,以供后面格式化使用。
重点只在于 operator()
。 它是一个变参模板,用来接取我们传入的任意类型和个数的参数,然后返回格式化后的字符串。
这里用到的是 std::vformat
进行格式化,它的第一个参数是格式字符串,也就是我们要按照什么样的规则去格式化;第二个参数是要格式化的参数,但是我们没有办法直接进行形参包展开,它第二个参数的类型实际上是 std::format_args
。
我们必须使用 std::make_format_args
函数传入我们的参数,它会返回 std::format_args
类型,其实也就是相当于转换一下,合理。
不过显然标准答案不是这样的,还能简化,直接让 ""_f
返回一个 lambda 表达式即可。
03
实现 print
以及特化 std::formatter
日期:2023/7/24
出题人:mq白
实现一个print
,如果你做了上一个作业,我相信这很简单。
要求调用形式为:
print(格式字符串,任意类型和个数的符合格式字符串要求的参数)
struct Frac {
int a, b;
};
给出自定义类型Frace
,要求支持
Frac f{ 1,10 };
print("{}", f);// 结果为1/10
1/10
禁止面向结果编程,使用宏等等方式,最多B
(指评价),本作业主要考察和学习format
库罢了。
提示: std::formatter
提交代码最好是网上编译了三个平台的截图,如:
template<>
struct std::formatter<Frac>:std::formatter<char>{
auto format(const auto& frac, auto& ctx)const{//const修饰是必须的
return std::format_to(ctx.out(), "{}/{}", frac.a, frac.b);
}
};
void print(std::string_view fmt,auto&&...args){
std::cout << std::vformat(fmt, std::make_format_args(args...));
}
我们只是非常简单的支持了题目要求的形式,给 std::formatter
进行特化,如果要支持比如那些 {:6}
之类的格式化的话,显然不行,这涉及到更多的操作。
简单的特化以及 std::formatter
支持的形式可以参见文档。
一些复杂的特化,up 之前也有写过,在 Cookbook 中,里面有对 std::ranges::range
和 std::tuple
的特化,支持所有形式。
实现一个 print 很简单,我们只要按第二题的思路来就行了,一个格式化字符串,用 std::string_view 做第一个形参,另外需要任意参数和个数,使用形参包即可。
void print(std::string_view fmt,auto&&...args){
std::cout << std::vformat(fmt, std::make_format_args(args...));
}
这样调用 vformat
,返回 string,可以使用 cout 直接输出。
而关于自定义 std::formatter
特化,我们需要知道的是:想要自定义 std::formatter 模板特化需要提供两个函数,parse 和 format。
parse 用来处理格式说明,并且设置相关的成员变量,对于本题我们不需要麻烦地实现此成员函数;
我们选择继承 std::formatter<char>
的 parse 函数,独立实现 format 函数。如果不了解此处模板特化的语法,请复习模板特化。
template<>
struct std::formatter<Frac> : std::formatter<char> {
auto format(const auto& frac, auto& ctx)const{//const修饰是必须的
return std::format_to(ctx.out(), "{}/{}", frac.a, frac.b);
}
};
我们同样使用 auto 作占位符的简写函数模板,对于 format 函数,首个参数为我们传递的自定义类,第二个参数(ctx)为我们要传递给 std::format_to
输出迭代器的格式字符串。
在函数体中我们直接返回 std::format_to()
调用表达式的结果,此函数返回输出迭代器;返回值我们使用 auto 占位符进行返回值推导。
在函数实参中,ctx.out()
即为输出迭代器,第二个参数为可转换为 std::string_view
或 std::wstring_view
,而转换结果是常量表达式和 Args 的合法格式字符串。本题中我们填入我们需要的形式,即 {}/{}
。
我们想要两个参数塞进 {}
,就如我们使用 printf(%d,x)
一样;最后两个参数为“需要塞进 {}
的值”,即要格式化的参数。
04
给定类模板修改,让其对每一个不同类型实例化有不同 ID日期:2023/7/25
出题人:Adttil
#include<iostream>
class ComponentBase{
protected:
static inline std::size_t component_type_count = 0;
};
template<typename T>
class Component : public ComponentBase{
public:
//todo...
//使用任意方式更改当前模板类,使得对于任意类型X,若其继承自Component
//则X::component_type_id()会得到一个独一无二的size_t类型的id(对于不同的X类型返回的值应不同)
//要求:不能使用std::type_info(禁用typeid关键字),所有id从0开始连续。
};
class A : public Component<A>
{};
class B : public Component<B>
{};
class C : public Component<C>
{};
int main()
{
std::cout << A::component_type_id() << std::endl;
std::cout << B::component_type_id() << std::endl;
std::cout << B::component_type_id() << std::endl;
std::cout << A::component_type_id() << std::endl;
std::cout << A::component_type_id() << std::endl;
std::cout << C::component_type_id() << std::endl;
}
0
1
1
0
0
2
提交应当给出多平台测试结果,如图:
template<typename T>
class Component : public ComponentBase{
public:
static std::size_t component_type_id(){
static std::size_t ID = component_type_count++;
return ID;
}
};
分析:
我们需要实现 Component
的静态成员函数 component_type_id
。这是从给出代码得知的:
class A : public Component<A>
{};
A::component_type_id()
题目要求是每一个自定义类类型(假设是 X)继承 Component<X>
,调用 component_type_id()
返回的是自己独一无二的 ID。其他的类型同理。
解决题目之前我们需要强调一个知识点:
C++ 的模板不是具体类型,实例化之后才是(即函数模板不是函数,类模板不是类),类模板的静态成员或静态成员函数也不属于模板,而是属于实例化后的具体类型,我们可以用一段代码来展示结论:
#include <iostream>
template<typename T>
struct Test{
inline static int n = 10;
};
int main(){
Test<int>::n = 1;
std::cout << Test<void>::n << 'n';//10
std::cout << Test<int>::n << 'n';//1
}
这段代码很轻易的就展示了静态数据成员属于模板实例化后的具体类型。Test<void>::n
和 Test<int>::n
不是相同的 n,并且 Test<void>
和 Test<int>
也不是一种类型(静态成员函数同理)。
所以我们的解法利用的是:不同的类型实例化 Component
类模板,也是不同的静态成员函数,静态成员函数里面的静态局部也都是唯一的,并且在第一次调用的时候才会初始化,后面就不会。
05
实现 scope_guard
类型日期:2023/7/29
出题人:Da'Inihlus
要求实现 scope_guard
类型 ( 即支持传入任意可调用类型 , 析构的时候同时调用 )。
#include <cstdio>
#include <cassert>
#include <stdexcept>
#include <iostream>
#include <functional>
struct X {
X() { puts("X()"); }
X(const X&) { puts("X(const X&)"); }
X(X&&) noexcept { puts("X(X&&)"); }
~X() { puts("~X()"); }
};
int main() {
{
// scope_guard的作用之一,是让各种C风格指针接口作为局部变量时也能得到RAII支持
// 这也是本题的基础要求
FILE * fp = nullptr;
try{
fp = fopen("test.txt","a");
auto guard = scope_guard([&] {
fclose(fp);
fp = nullptr;
});
throw std::runtime_error{"Test"};
} catch(std::exception & e){
puts(e.what());
}
assert(fp == nullptr);
}
puts("----------");
{
// 附加要求1,支持函数对象调用
struct Test {
void operator()(X* x) {
delete x;
}
} t;
auto x = new X{};
auto guard = scope_guard(t, x);
}
puts("----------");
{
// 附加要求2,支持成员函数和std::ref
auto x = new X{};
{
struct Test {
void f(X*& px) {
delete px;
px = nullptr;
}
} t;
auto guard = scope_guard{&Test::f, &t, std::ref(x)};
}
assert(x == nullptr);
}
}
Test
----------
X()
~X()
----------
X()
~X()
std::function
并擦除类型struct scope_guard {
std::function<void()>f;
template<typename Func, typename...Args> requires std::invocable<Func, std::unwrap_reference_t<Args>...>
scope_guard(Func&& func, Args&&...args) :f{ [func = std::forward<Func>(func), ...args = std::forward<Args>(args)]() mutable {
std::invoke(std::forward<std::decay_t<Func>>(func), std::unwrap_reference_t<Args>(std::forward<Args>(args))...);
} }{}
~scope_guard() { f(); }
scope_guard(const scope_guard&) = delete;
scope_guard& operator=(const scope_guard&) = delete;
};
std::tuple
+std::apply
template<typename F, typename...Args>
requires requires(F f, Args...args) { std::invoke(f, args...); }
struct scope_guard {
F f;
std::tuple<Args...>values;
template<typename Fn, typename...Ts>
scope_guard(Fn&& func, Ts&&...args) :f{ std::forward<Fn>(func) }, values{ std::forward<Ts>(args)... } {}
~scope_guard() {
std::apply(f, values);
}
scope_guard(const scope_guard&) = delete;
};
template<typename F, typename...Args>//推导指引非常重要
scope_guard(F&&, Args&&...) -> scope_guard<std::decay_t<F>, std::decay_t<Args>...>;
06
解释 std::atomic
初始化日期:2023/8/2
出题人:mq白
#include <iostream>
#include <atomic>
int main() {
std::atomic<int> n = 6;
std::cout << n << 'n';
}
解释,为什么以上代码在 C++17 后可以通过编译,C++17 前不行?
std::atomic<int> n = 6
中,由于 6
和 std::atomic<int>
不是同一类型(但是这里其实有一个用户定义转换序列,你可以简单的认为6
可以隐式转换)。
即调用转换构造函数:
constexpr atomic( T desired ) noexcept;
转换构造函数也会作为用户定义的转换序列中的一部分
6
会调用转换构造函数,构造出一个临时的 atomic 对象用来直接初始化 n
,即
std::atomic<int> n(std::atomic<int>(6))
在 C++17 之前 的版本,理所应当应该查找检测复制/移动 构造函数,满足要求才可以通过编译。但是: