01
實作管道運算符02
實作自訂字面量_f
03
實作print
以及特化std::formatter
04
給定類別模板修改,讓其對每一個不同類型實例化有不同ID05
實作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 之前的版本,理所當然應尋找檢測複製/移動構造函數,滿足要求才可透過編譯。但是: