01
den Pipeline-Operator02
Implementieren Sie das benutzerdefinierte Literal _f
03
Implementieren Sie print
und spezialisieren Sie std::formatter
04
Ändern Sie die angegebene Klassenvorlage so, dass sie für jede Instanziierung verschiedener Typen eine andere ID hat05
scope_guard
Typ06
der std::atomic
Initialisierung07
throw new MyException
08
des array
Ableitungsleitfadens09
bei der Namenssuche10
Durchlaufen Sie alle Datenelemente der KlasseC++17
SchreibmethodeC++20
Schreibmethode11
Probleme mit emplace_back()
make_vector()
12
13
return std::move
14
Ändern Sie im Namespace deklarierte Objekte in speziellen Methoden15
Ausdrucksvorlagen16
Makros zum Erstellen von ÜbertragungsfunktionsvorlagenLuthors Hausaufgaben-Ausstellung.
Das Senden einer PR sollte die aktuelle README
nicht ändern. Bitte senden Sie den Job an src群友提交
. Wenn Sie beispielsweise den ersten Job senden möchten:
Sie sollten Ihre eigene .md
oder .cpp
Datei in src群友提交第01题
erstellen. Der Dateiname sollte nach Ihrer eigenen Kommunikationsgruppen-ID (oder Ihrem GitHub-Benutzernamen, damit Sie sich leicht wiederfinden) benannt sein .
Die allgemeinen Anforderungen für die Beantwortung von Fragen sind wie folgt (beachten Sie die zusätzlichen Anforderungen für die Fragen):
main
nicht ändern, dürfen Sie ihre Ausführung nicht stoppen (d. h. sie nicht ausnutzen).01
den Pipeline-Operator Datum: 2023/7/21
Fragesteller: mq白
Angesichts des Codes:
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
Beantwortet von: 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;
}
Es ist normal, kein Problem.
Beantwortet von: 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;
}
Kommentar: Wenn ich nichts zu tun habe, werde ich weitere Überladungen schreiben und sie einrahmen.
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;
}
Ohne Verwendung von Vorlagen :
std::vector< int >& operator |(std::vector< int >& v1, const std::function< void ( int &)>& f) {
for ( auto & i : v1) {
f (i);
}
return v1;
}
Verwenden Sie statt eines „scoped for
“ die C++20-Kurzfunktionsvorlage:
std::vector< int >& operator |( auto & v1, const auto & f) {
std::ranges::for_each (v1, f);
return v1;
}
Die Paradigmen verschiedener anderer Antworten sind nichts anderes als diese Änderungen, es besteht keine Notwendigkeit, sie noch einmal zu schreiben.
Offensichtlich müssen wir den Pipeline-Operator | überladen. Gemäß unserer Aufrufform v | f2 | f
wird geändert. v | f2
ruft operator |
auf. Der Operator | verwendet f2, um jedes Element in v zu durchlaufen, und gibt dann | zurück.
template < typename U, typename F>
requires std::regular_invocable<F, U&> //我们可以认为对模板形参U,F满足std::regular_invocable的约束
Wenn Sie noch nie mit Einschränkungsausdrücken in Berührung gekommen sind, spielt das keine Rolle. Wir werden sie im Folgenden kurz vorstellen.
Der require-Ausdruck ist wie eine Funktion, die bool zurückgibt, und U und F werden als Typen in die tatsächliche Parameterliste von std::regular_invocable eingetragen. Solange U und F als Typen den Ausdruck erfüllen, wird true zurückgegeben , es gibt false zurück, was als „Einschränkung nicht erfüllt“ bezeichnet wird. Typen, die die Einschränkungen nicht erfüllen, führen natürlich keinen nachfolgenden Code aus.
Was std::regular_invocable betrifft, können wir es uns einfach als ein Paar jedes Werts vom Typ U vorstellen. Ob wir Funktion F aufrufen können, also std::invoke
aufrufen.
Dies ist gleichbedeutend damit, dass wir uns die Laufzeit zur Kompilierungszeit vorstellen und uns vorstellen, ob U F zur Laufzeit ausführen kann. Wenn ja, erfüllt es die Einschränkungen.
Der Funktionskörper ist äußerst einfach
std::vector<U>& operator |(std::vector<U>& v1, const F f) {
for ( auto & i : v1) {
f (i);
}
return v1;
}
Der Bereichsausdruck for (auto& i : v1)
ist wie for(auto i=v.begin();i!=v.end();++i){f(*i)}
: wir haben vector (range ) wendet die f -Funktion einmal auf jedes darin enthaltene Element an. Kehrt wie gewohnt zu Version 1 zurück.
Wenn wir keine Vorlagen verwenden, muss unsere formale Parameterliste std::function verwenden, um die von uns verwendete Funktion abzufangen:
Das Anwenden von f auf jedes Mitglied des Bereichs erfordert keinen Rückgabewert und erfordert eine Änderung der Elemente im Bereich, daher ist der zweite Parameter std::function<void(int&)>
. Und wir müssen die übergebene Funktion f nicht ändern oder kopieren, daher ist es eine gute Angewohnheit, konstante Einschränkungen hinzuzufügen.
Ebenso können wir range for nicht verwenden, sondern das einfachere std::ranges::for_each(v1, f);
das heißt, wir wenden die Funktion f einmal auf jedes Element im Bereich v1 wie oben an.
Für die Form der Verwendung von Vorlagen können wir die C++20-Abkürzungsfunktionsvorlage verwenden. Kurz gesagt, der automatische Platzhalter in der Funktionsparameterliste hängt einen Dummy-Vorlagenparameter an die Vorlagenparameterliste an. Das ursprüngliche Vorlagenformular kann wie folgt geschrieben werden:
std::vector< int >& operator |( auto & v1, const auto & f)
Es ist das gleiche wie die Originalform.
02
Implementieren Sie das benutzerdefinierte Literal _f
Datum: 2023/7/22
Fragesteller: mq白
Angesichts des Codes:
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
ist die Eingabe und bestimmt
Beantwortet von: 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...)); };
}
Wir müssen benutzerdefinierte C++11-Literale verwenden, und ""_f
ist genau das benutzerdefinierte Literal.
Die formale Parameterliste des Literaloperators (die vom benutzerdefinierten Literal aufgerufene Funktion wird als Literaloperator bezeichnet) weist jedoch einige Einschränkungen auf. Wir benötigen eine formale Parameterliste wie const char *, std::size_t
, die Zufälligerweise ist dies zulässig; der Rückgabetyp eines Literaloperators muss angepasst werden, und dieser Typ muss operator()
intern überladen, um die oben genannten Anforderungen zu erfüllen, damit Literale wie Funktionen aufgerufen werden können.
Gehen wir Schritt für Schritt vor:
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
Die beiden Anwendungsbeispiele des obigen Codes zeigen die grundlegende Verwendung unserer benutzerdefinierten Literale. Achten Sie besonders auf den zweiten Absatz, den Rückgabewert . Wenn Sie es wie "xxx"_f(xxx)
nennen möchten, müssen Sie etwas mit dem Rückgabetyp tun.
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
Der obige einfache Code vervollständigt das von uns benötigte Aufrufformular perfekt. Anschließend ist es an der Zeit, die für die Frage erforderlichen Funktionen auszuführen. Am einfachsten ist es, zur Formatierung direkt die C++20-Formatbibliothek zu verwenden.
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
selbst ist sehr einfach. Er wird lediglich verwendet, um impl::Helper
Objekt aus den eingehenden Parametern (Formatzeichenfolge) und der Länge zu erstellen und es dann zurückzugeben. Der Helper
-Typ verwendet string_view
als Datenelement, um die Formatzeichenfolge für eine spätere Formatierung zu speichern.
Der Fokus liegt nur auf operator()
. Es handelt sich um eine Vorlage für variable Parameter, die zum Empfangen beliebiger Art und Anzahl von Parametern verwendet wird, die wir übergeben, und die dann eine formatierte Zeichenfolge zurückgibt.
Was hier verwendet wird, ist std::vformat
. Sein erster Parameter ist die Formatzeichenfolge, das heißt, nach welchen Regeln wir formatieren möchten; der zweite Parameter ist der Parameter, der formatiert werden soll, aber wir können dies nicht direkt tun Erweitern Sie das formale Parameterpaket. Der Typ seines zweiten Parameters ist tatsächlich std::format_args
. Wir müssen std::make_format_args
verwenden, um unsere Parameter zu übergeben. Sie gibt std::format_args
zurück. Tatsächlich entspricht dies einer Konvertierung, was sinnvoll ist.
Aber offensichtlich ist die Standardantwort nicht so und kann vereinfacht werden, indem man ""_f
einfach einen Lambda-Ausdruck zurückgeben lässt.
03
Implementieren Sie print
und spezialisieren Sie std::formatter
Datum: 2023/7/24
Fragesteller: mq白
Wenn Sie die vorherige Aufgabe erledigt haben, ist die Implementierung eines print
meiner Meinung nach einfach. Das erforderliche Aufrufformular lautet:
print (格式字符串,任意类型和个数的符合格式字符串要求的参数)
struct Frac {
int a, b;
};
Fordern Sie bei einem benutzerdefinierten Typ Frace
Unterstützung an
Frac f{ 1 , 10 };
print ( " {} " , f); // 结果为1/10
1/10
Ergebnisorientiertes Programmieren, die Verwendung von Makros usw. sind mit maximal B
(bezogen auf die Bewertung) verboten. Bei dieser Aufgabe wird hauptsächlich format
untersucht und erlernt.
Tipp: std::formatter
Am besten reichen Sie Code mit Screenshots von drei online zusammengestellten Plattformen ein, wie zum Beispiel:
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...));
}
Wir unterstützen lediglich die von der Frage geforderte Form und spezialisieren uns std::formatter
. Wenn wir Formatierungen wie {:6}
unterstützen möchten, ist dies natürlich nicht möglich. Einfache Spezialisierungen und Formen, die von std::formatter
unterstützt werden, finden Sie in der Dokumentation . Einige komplexe Spezialisierungen wurden von up bereits geschrieben. Im Cookbook gibt es Spezialisierungen für std::ranges::range
und std::tuple
, die alle Formen unterstützen.
Die Implementierung eines Drucks ist sehr einfach. Für eine formatierte Zeichenfolge verwenden Sie einfach std::string_view das formale Parameterpaket.
void print (std::string_view fmt, auto &&...args){
std::cout << std::vformat (fmt, std::make_format_args (args...));
}
Ein solcher Aufruf vformat
gibt einen String zurück, der direkt mit cout ausgegeben werden kann.
Was die benutzerdefinierte std::formatter
Spezialisierung betrifft, müssen wir Folgendes wissen: Wenn Sie die std::formatter -Vorlagenspezialisierung anpassen möchten, müssen Sie zwei Funktionen bereitstellen: parse und format .
Parse wird verwendet, um Formatbeschreibungen zu verarbeiten und zugehörige Mitgliedsvariablen festzulegen. Für diese Frage müssen wir uns nicht die Mühe machen, diese Mitgliedsfunktion zu implementieren.
Wir entscheiden uns dafür, die Parse- Funktion von std::formatter<char>
zu erben und die Formatfunktion unabhängig zu implementieren. Wenn Sie die Syntax der Vorlagenspezialisierungen hier nicht verstehen, lesen Sie den Abschnitt „Vorlagenspezialisierungen“.
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 );
}
};
Wir verwenden auto auch als Kurzfunktionsvorlage für Platzhalter. Für die Formatfunktion ist der erste Parameter die benutzerdefinierte Klasse, die wir übergeben, und der zweite Parameter ( ctx ) ist das Formatzeichen, das wir an std::format_to
übergeben möchten . Zeichenfolge.
Im Funktionskörper geben wir direkt das Ergebnis std::format_to()
zurück. Diese Funktion gibt den Ausgabeiterator für den Rückgabewert zurück. Wir verwenden den Auto- Platzhalter für die Ableitung des Rückgabewerts.
Unter den Funktionsparametern ist ctx.out()
der Ausgabeiterator, der zweite Parameter ist eine Zeichenfolge im zulässigen Format, die in std::string_view
oder std::wstring_view
konvertiert werden kann, und das Konvertierungsergebnis ist ein konstanter Ausdruck und Argumente. In dieser Frage füllen wir das benötigte Formular aus, nämlich {}/{}
.
Wir möchten, dass die beiden Parameter in {}
gestopft werden, genau wie wir printf(%d,x)
verwenden; die letzten beiden Parameter sind die „Werte, die in {}
gestopft werden müssen“, also die Parameter von formatiert werden.
04
Ändern Sie die angegebene Klassenvorlage so, dass sie für jede Instanziierung verschiedener Typen eine andere ID hat Datum: 2023/7/25
Fragesteller: 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
Die Einreichung sollte plattformübergreifende Testergebnisse liefern, wie in der Abbildung dargestellt:
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;
}
};
analysieren:
Wir müssen die statische Memberfunktion component_type_id
von Component
implementieren. Dies ist aus dem angegebenen Code bekannt:
class A : public Component <A>
{};
A::component_type_id ()
Die Frage erfordert, dass jeder benutzerdefinierte Klassentyp (angenommen X) Component<X>
erbt und der Aufruf von component_type_id()
seine eigene eindeutige ID zurückgibt. Das Gleiche gilt auch für andere Typen.
Bevor wir das Problem lösen, müssen wir einen Wissenspunkt hervorheben:
C++-Vorlagen sind keine spezifischen Typen, sondern nach der Instanziierung (d. h. Funktionsvorlagen sind keine Funktionen und Klassenvorlagen sind keine Klassen ). Statische Mitglieder oder statische Mitgliedsfunktionen von Klassenvorlagen gehören nicht zu Vorlagen, sondern zu bestimmten Typen nach der Instanziierung . Wir können einen Code verwenden, um die Schlussfolgerung zu demonstrieren:
# 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
}
Dieser Code zeigt leicht, dass statische Datenelemente nach der Instanziierung der Vorlage zum spezifischen Typ gehören . Test<void>::n
und Test<int>::n
sind nicht dasselbe n, und Test<void>
und Test<int>
sind nicht vom gleichen Typ (dasselbe gilt für statische Memberfunktionen).
Unsere Lösung verwendet also: Verschiedene Arten von instanziierten Component
sind auch unterschiedliche statische Mitgliedsfunktionen. Die statischen Teile in den statischen Mitgliedsfunktionen sind ebenfalls eindeutig und werden nur beim ersten Aufruf initialisiert.
05
scope_guard
Typ Datum: 2023/7/29
Fragesteller: Da'Inihlus
Es ist erforderlich, den Typ scope_guard
zu implementieren (d. h. es unterstützt die Übergabe eines beliebigen aufrufbaren Typs und dessen gleichzeitigen Aufruf während der Zerstörung).
# 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
und löschen Sie den Typ 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
der std::atomic
Initialisierung Datum: 2023/8/2
Fragesteller: mq白
# include < iostream >
# include < atomic >
int main () {
std::atomic< int > n = 6 ;
std::cout << n << ' n ' ;
}
Erklären Sie, warum der obige Code nach C++17, aber nicht vor C++17 kompiliert werden kann.
Da in std::atomic<int> n = 6
6
und std::atomic<int>
nicht vom gleichen Typ sind (es gibt hier jedoch tatsächlich eine benutzerdefinierte Konvertierungssequenz), können Sie einfach denken, dass 6
implizit konvertiert werden kann ).
Das heißt, den Konvertierungskonstruktor aufrufen:
constexpr atomic ( T desired ) noexcept ;
Konvertierungskonstruktoren werden auch als Teil einer benutzerdefinierten Konvertierungssequenz verwendet
6
ruft den Konvertierungskonstruktor auf, um ein temporäres Atomobjekt zu erstellen und n
direkt zu initialisieren
std::atomic< int > n (std::atomic< int >( 6 ))
In Versionen vor C++17 ist es selbstverständlich, dass der Copy/Move-Konstruktor durchsucht und erkannt werden sollte, bevor er kompiliert werden kann, wenn er die Anforderungen erfüllt. Aber: