01
l'opérateur de pipeline02
Implémenter le littéral personnalisé _f
03
Implémenter print
et spécialiser std::formatter
04
Modifier le modèle de classe donné afin qu'il ait un ID différent pour chaque instanciation de type différent05
le type scope_guard
06
de std::atomic
07
throw new MyException
08
du guide de dérivation array
09
10
Parcourez tous les membres de données de classeC++17
C++20
11
Problèmes avec emplace_back()
12
make_vector()
13
return std::move
14
Modifier les objets déclarés dans l'espace de noms dans des méthodes spéciales15
modèles d'expressions16
Macros pour créer des modèles de fonctions de transfertAffichage des devoirs de Luthor.
La soumission d'un PR ne devrait pas modifier le README
actuel. Veuillez soumettre le travail à src群友提交
. Par exemple, si vous souhaitez soumettre le premier travail :
Vous devez créer votre propre fichier .md
ou .cpp
dans src群友提交第01题
Le nom du fichier doit être nommé d'après votre propre ID de groupe de communication (ou nom d'utilisateur GitHub, afin que vous puissiez vous retrouver facilement) .
Les exigences générales pour répondre aux questions sont les suivantes (faites attention aux exigences supplémentaires pour les questions) :
main
, vous ne devez pas l'empêcher de fonctionner (c'est-à-dire ne pas en profiter).01
l'opérateur de pipeline Date : 2023/7/21
Interrogateur : mq白
Étant donné le code :
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
Répondu par: 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;
}
C'est normal, pas de problème.
Répondu par: 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;
}
Commentaire : Si je n'ai rien à faire, j'écrirai plus de surcharges et les encadrerai.
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;
}
Sans utiliser de modèles :
std::vector< int >& operator |(std::vector< int >& v1, const std::function< void ( int &)>& f) {
for ( auto & i : v1) {
f (i);
}
return v1;
}
Au lieu d'utiliser un scoped for
, utilisez le modèle de fonction abrégée C++20 :
std::vector< int >& operator |( auto & v1, const auto & f) {
std::ranges::for_each (v1, f);
return v1;
}
Les paradigmes de diverses autres réponses ne sont rien d’autre que ces changements, il n’est pas nécessaire de les réécrire.
Évidemment, nous devons surcharger l'opérateur de pipeline |. D'après notre forme d'appel v | f2 | f
, et sur la base des résultats d'exécution donnés, nous pouvons savoir que la fonction surchargée doit renvoyer une référence à v et v. sera modifié. v | f2
appelle operator |
. L'opérateur | utilise f2 pour parcourir chaque élément de v, puis renvoie la référence de v, puis |
template < typename U, typename F>
requires std::regular_invocable<F, U&> //我们可以认为对模板形参U,F满足std::regular_invocable的约束
Si vous n’avez jamais été exposé aux expressions de contraintes, cela n’a pas d’importance. Nous les présenterons brièvement ci-dessous.
L'expression require est comme une fonction qui renvoie bool, et U et F sont remplis dans la liste de paramètres réelle de std::regular_invocable en tant que types Tant que U et F en tant que types satisfont l'expression, elle renvoie true si ce n'est pas le cas. , il renvoie false, ce qu'on appelle "contrainte non satisfaite". Les types qui ne satisfont pas aux contraintes n’exécuteront naturellement pas le code suivant.
Quant à std::regular_invocable, nous pouvons simplement le considérer comme une paire de chaque valeur de type U. Si nous pouvons appeler la fonction F, c'est-à-dire appeler std::invoke
.
Cela équivaut à imaginer le moment de l'exécution au moment de la compilation et à imaginer si U peut exécuter F au moment de l'exécution. Si tel est le cas, il satisfait aux contraintes.
Le corps de la fonction est extrêmement simple
std::vector<U>& operator |(std::vector<U>& v1, const F f) {
for ( auto & i : v1) {
f (i);
}
return v1;
}
L'expression de plage for (auto& i : v1)
est comme for(auto i=v.begin();i!=v.end();++i){f(*i)}
: nous avons un vecteur (range ) applique la fonction f une fois à chaque élément qu'il contient. Revient à la v1 comme d'habitude.
Si nous n'utilisons pas de modèles, notre liste de paramètres formels doit utiliser std::function pour capturer la fonction que nous utilisons :
L'application de f à chaque membre de la plage ne nécessite pas de valeur de retour et nécessite une modification des éléments de la plage, donc le deuxième paramètre est std::function<void(int&)>
. Et nous n'avons pas besoin de modifier ou de copier la fonction f transmise, c'est donc une bonne habitude d'ajouter des restrictions const .
De même, nous ne pouvons pas utiliser range for mais le plus simple std::ranges::for_each(v1, f);
c'est-à-dire appliquer la fonction f une fois à chaque élément de la plage v1 comme ci-dessus.
Pour la forme d'utilisation des modèles, nous pouvons utiliser le modèle de fonction d'abréviation C++20 ; en bref, l'espace réservé automatique dans la liste des paramètres de fonction ajoutera un paramètre de modèle factice à la liste des paramètres du modèle. Le formulaire de modèle initial peut être écrit comme
std::vector< int >& operator |( auto & v1, const auto & f)
C'est la même chose que la forme originale.
02
Implémenter le littéral personnalisé _f
Date : 2023/7/22
Interrogateur : mq白
Étant donné le code :
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
est l'entrée et détermine
Répondu par: 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...)); };
}
Nous devons utiliser des littéraux définis par l'utilisateur C++ 11, et ""_f
est exactement le littéral défini par l'utilisateur.
Cependant, la liste formelle des paramètres de l'opérateur littéral (la fonction appelée par le littéral défini par l'utilisateur est appelée l'opérateur littéral) a certaines restrictions. Ce dont nous avons besoin est une liste formelle de paramètres telle que const char *, std::size_t
, qui. il se trouve que cela est autorisé ; le type de retour d'un opérateur littéral doit être personnalisé, et ce type doit surcharger operator()
en interne pour répondre aux exigences ci-dessus pour que les littéraux soient appelés comme des fonctions.
Allons-y étape par étape :
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
Les deux exemples d'utilisation du code ci-dessus montrent l'utilisation de base de nos littéraux définis par l'utilisateur, accordez une attention particulière au deuxième paragraphe, valeur de retour . Si vous voulez l'appeler comme "xxx"_f(xxx)
, vous devez faire quelque chose avec le type de retour.
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
Le code simple ci-dessus complète parfaitement le formulaire d'appel dont nous avons besoin, il est alors temps de compléter les fonctions requises par la question. Le moyen le plus simple consiste à utiliser la bibliothèque de format C++20 directement pour le formatage.
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
lui-même est très simple. Il est simplement utilisé pour construire impl::Helper
à partir des paramètres entrants (chaîne de format) et de la longueur, puis le renvoyer. Le type Helper
utilise un string_view
comme donnée membre pour stocker la chaîne de format pour un formatage ultérieur.
L'accent est uniquement mis sur operator()
. Il s'agit d'un modèle de paramètre variable, utilisé pour recevoir n'importe quel type et nombre de paramètres que nous transmettons, puis renvoyer une chaîne formatée.
Ce qui est utilisé ici est std::vformat
pour le formatage. Son premier paramètre est la chaîne de format, c'est-à-dire les règles selon lesquelles nous voulons formater, le deuxième paramètre est le paramètre à formater, mais il n'y a aucun moyen de le faire directement. développez le package de paramètres formels. Le type de son deuxième paramètre est en fait std::format_args
. Nous devons utiliser std::make_format_args
pour passer nos paramètres. Elle renverra std::format_args
. En fait, cela équivaut à une conversion, ce qui est raisonnable.
Mais évidemment, la réponse standard n'est pas comme ça, et elle peut être simplifiée en laissant simplement ""_f
renvoyer une expression lambda.
03
Implémenter print
et spécialiser std::formatter
Date : 2023/7/24
Interrogateur : mq白
Implémenter un print
, si vous avez fait la mission précédente, je pense que c'est simple. Le formulaire d'appel requis est :
print (格式字符串,任意类型和个数的符合格式字符串要求的参数)
struct Frac {
int a, b;
};
Étant donné un type personnalisé Frace
, demandez de l'aide
Frac f{ 1 , 10 };
print ( " {} " , f); // 结果为1/10
1/10
La programmation orientée résultats, l'utilisation de macros, etc. sont interdites, avec un maximum de B
(se référant à l'évaluation). Ce devoir examine et apprend principalement format
.
Astuce : std::formatter
Il est préférable de soumettre du code avec des captures d'écran de trois plateformes compilées en ligne, telles que :
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...));
}
Nous prenons simplement en charge le formulaire requis par la question et spécialisons std::formatter
. Si nous voulons prendre en charge un formatage tel que {:6}
, cela n'est évidemment pas possible. Des spécialisations et des formulaires simples pris en charge par std::formatter
peuvent être trouvés dans la documentation . Certaines spécialisations complexes ont déjà été écrites par Up. Dans le Cookbook , il existe des spécialisations pour std::ranges::range
et std::tuple
, prenant en charge toutes les formes.
L'implémentation d'un print est très simple. Il suffit de suivre l'idée de la deuxième question. Pour une chaîne formatée, utilisez std::string_view comme premier paramètre formel. De plus, si des paramètres et des nombres sont nécessaires, utilisez simplement. le package de paramètres formels.
void print (std::string_view fmt, auto &&...args){
std::cout << std::vformat (fmt, std::make_format_args (args...));
}
Appeler vformat
de cette manière renvoie une chaîne, qui peut être générée directement à l'aide de cout.
Concernant la spécialisation personnalisée std::formatter
, ce que nous devons savoir est : si vous souhaitez personnaliser la spécialisation de modèle std::formatter , vous devez fournir deux fonctions, parse et format .
parse est utilisé pour traiter les descriptions de format et définir les variables membres associées. Pour cette question, nous n'avons pas besoin de nous donner la peine d'implémenter cette fonction membre ;
Nous choisissons d'hériter de la fonction d'analyse de std::formatter<char>
et d'implémenter la fonction de format indépendamment. Si vous ne comprenez pas la syntaxe des spécialisations de modèles ici, consultez Spécialisations de modèles.
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 );
}
};
Nous utilisons également auto comme modèle de fonction raccourci pour les espaces réservés. Pour la fonction de format , le premier paramètre est la classe personnalisée que nous transmettons et le deuxième paramètre ( ctx ) est le caractère de format que nous voulons transmettre à std::format_to
. chaîne.
Dans le corps de la fonction, nous renvoyons directement le résultat de std::format_to()
. Cette fonction renvoie l'itérateur de sortie ; pour la valeur de retour, nous utilisons l'espace réservé automatique pour la dérivation de la valeur de retour.
Parmi les paramètres de fonction, ctx.out()
est l'itérateur de sortie, le deuxième paramètre est une chaîne de format légal qui peut être convertie en std::string_view
ou std::wstring_view
, et le résultat de la conversion est une expression constante et Args. Dans cette question, nous remplissons le formulaire dont nous avons besoin, à savoir {}/{}
.
Nous voulons que les deux paramètres soient insérés dans {}
, tout comme nous utilisons printf(%d,x)
; les deux derniers paramètres sont les "valeurs qui doivent être insérées dans {}
", c'est-à-dire les paramètres à être formaté.
04
Modifier le modèle de classe donné afin qu'il ait un ID différent pour chaque instanciation de type différent Date : 2023/7/25
Intervenant : 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
La soumission doit fournir des résultats de tests multiplateformes, comme le montre la figure :
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;
}
};
analyser:
Nous devons implémenter la fonction membre statique component_type_id
de Component
. Ceci est connu grâce au code donné :
class A : public Component <A>
{};
A::component_type_id ()
La question nécessite que chaque type de classe personnalisée (supposé être X) hérite Component<X>
et que l'appel de component_type_id()
renvoie son propre ID unique. Il en va de même pour les autres types.
Avant de résoudre le problème, nous devons souligner un point de connaissance :
Les modèles C++ ne sont pas des types spécifiques , ils le sont après l'instanciation (c'est-à-dire que les modèles de fonction ne sont pas des fonctions et les modèles de classe ne sont pas des classes ). . Nous Vous pouvez utiliser un morceau de code pour démontrer la conclusion :
# 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
}
Ce code montre facilement que les données membres statiques appartiennent à un type spécifique après l'instanciation du modèle . Test<void>::n
et Test<int>::n
ne sont pas les mêmes n, et Test<void>
et Test<int>
ne sont pas du même type (il en va de même pour les fonctions membres statiques).
Notre solution utilise donc : différents types de modèles de classes Component
instanciés sont également différentes fonctions membres statiques. Les parties statiques des fonctions membres statiques sont également uniques et ne seront initialisées que lors de leur premier appel.
05
le type scope_guard
Date : 2023/7/29
Intervenant : Da'Inihlus
Il est nécessaire d'implémenter le type scope_guard
(c'est-à-dire qu'il prend en charge la transmission de n'importe quel type appelable et son appel en même temps lors de la destruction).
# 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
et effacez le type 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
de std::atomic
Date : 2023/8/2
Interrogateur : mq白
# include < iostream >
# include < atomic >
int main () {
std::atomic< int > n = 6 ;
std::cout << n << ' n ' ;
}
Expliquez pourquoi le code ci-dessus peut être compilé après C++17 mais pas avant C++17 ?
Dans std::atomic<int> n = 6
, puisque 6
et std::atomic<int>
ne sont pas du même type (mais il y a en fait une séquence de conversion définie par l'utilisateur ici, vous pouvez simplement penser que 6
peut être implicitement converti ).
Autrement dit, appeler le constructeur de conversion :
constexpr atomic ( T desired ) noexcept ;
Les constructeurs de conversion sont également utilisés dans le cadre d'une séquence de conversion définie par l'utilisateur.
6
appellera le constructeur de conversion pour construire un objet atomique temporaire pour initialiser directement n
, c'est-à-dire
std::atomic< int > n (std::atomic< int >( 6 ))
Dans les versions antérieures à C++17 , il est naturel que le constructeur de copie/déplacement soit recherché et détecté avant de pouvoir être compilé s'il répond aux exigences. mais: