Aucun amour C ++ quand il s'agit de la ligne de questions «fonctionnalités cachées»? Je pensais que je le jetterais là-bas. Quelles sont certaines des fonctionnalités cachées de C ++?
Aucun amour C ++ quand il s'agit de la ligne de questions «fonctionnalités cachées»? Je pensais que je le jetterais là-bas. Quelles sont certaines des fonctionnalités cachées de C ++?
Réponses:
La plupart des programmeurs C ++ sont familiers avec l'opérateur ternaire:
x = (y < 0) ? 10 : 20;
Cependant, ils ne réalisent pas qu'il peut être utilisé comme une lvalue:
(a == 0 ? a : b) = 1;
qui est un raccourci pour
if (a == 0)
a = 1;
else
b = 1;
Utiliser avec précaution :-)
(value ? function1 : function2)()
.
function1
et function2
sont implicitement convertis en pointeurs de fonction, et le résultat est implicitement reconverti.
Vous pouvez placer des URI dans la source C ++ sans erreur. Par exemple:
void foo() {
http://stackoverflow.com/
int bar = 4;
...
}
goto
, ce que C ++ a). Tout ce qui suit deux barres obliques est un commentaire. Par conséquent, avec http://stackoverflow.com
, http
est une étiquette (vous pourriez théoriquement écrire goto http;
), et //stackoverflow.com
n'est qu'un commentaire de fin de ligne. Les deux sont du C ++ légal, donc la construction se compile. Cela ne fait rien de vaguement utile, bien sûr.
goto http;
ne suit pas réellement l'URL. :(
Arithmétique de pointeur.
Les programmeurs C ++ préfèrent éviter les pointeurs à cause des bogues qui peuvent être introduits.
Le C ++ le plus cool que j'aie jamais vu? Littéraux analogiques.
Je suis d'accord avec la plupart des articles: C ++ est un langage multi-paradigme, donc les fonctionnalités "cachées" que vous trouverez (autres que les "comportements indéfinis" que vous devriez éviter à tout prix) sont des utilisations intelligentes des installations.
La plupart de ces fonctionnalités ne sont pas des fonctionnalités intégrées du langage, mais des fonctionnalités basées sur une bibliothèque.
Le plus important est le RAII , souvent ignoré depuis des années par les développeurs C ++ venant du monde C. La surcharge des opérateurs est souvent une fonctionnalité mal comprise qui permet à la fois un comportement de type tableau (opérateur d'indice), des opérations de type pointeur (pointeurs intelligents) et des opérations de type intégré (multiplication des matrices.
L'utilisation de l' exception est souvent difficile, mais avec un peu de travail, elle peut produire un code vraiment robuste grâce à des spécifications de sécurité d'exception (y compris du code qui n'échouera pas, ou qui aura des fonctionnalités de type commit qui réussiront ou reviendront à son état d'origine).
La fonctionnalité "cachée" la plus connue de C ++ est la métaprogrammation de modèles , car elle vous permet d'exécuter partiellement (ou totalement) votre programme au moment de la compilation au lieu de l'exécution. C'est difficile, cependant, et vous devez avoir une solide maîtrise des modèles avant de l'essayer.
D'autres utilisent le paradigme multiple pour produire des «façons de programmer» en dehors de l'ancêtre de C ++, c'est-à-dire C.
En utilisant des foncteurs , vous pouvez simuler des fonctions, avec la sécurité de type supplémentaire et étant avec état. En utilisant le modèle de commande , vous pouvez retarder l'exécution du code. La plupart des autres modèles de conception peuvent être facilement et efficacement implémentés en C ++ pour produire des styles de codage alternatifs qui ne sont pas censés être dans la liste des «paradigmes officiels du C ++».
En utilisant des modèles , vous pouvez produire du code qui fonctionnera sur la plupart des types, y compris celui que vous avez pensé au début. Vous pouvez également augmenter la sécurité des types (comme un malloc / realloc / free). Les fonctionnalités des objets C ++ sont vraiment puissantes (et donc dangereuses si elles sont utilisées avec négligence), mais même le polymorphisme dynamique a sa version statique en C ++: le CRTP .
J'ai trouvé que la plupart des livres de type " C ++ efficace " de Scott Meyers ou des livres de type " C ++ exceptionnel " de Herb Sutter sont à la fois faciles à lire et sont assez riches d'informations sur les fonctionnalités connues et moins connues du C ++.
Parmi mes préférés, il y en a un qui devrait faire sortir les cheveux de tout programmeur Java de l'horreur: en C ++, le moyen le plus orienté objet d'ajouter une fonctionnalité à un objet est via une fonction non-membre non-amie, au lieu d'un membre- fonction (c'est-à-dire méthode de classe), car:
En C ++, l'interface d'une classe est à la fois ses fonctions membres et les fonctions non membres dans le même espace de noms
les fonctions non-amies non-membres n'ont pas d'accès privilégié à la classe interne. En tant que tel, l'utilisation d'une fonction membre sur une fonction non-amie non-membre affaiblira l'encapsulation de la classe.
Cela ne manque jamais de surprendre même les développeurs expérimentés.
(Source: Entre autres, le gourou en ligne de Herb Sutter # 84: http://www.gotw.ca/gotw/084.htm )
Une caractéristique du langage que je considère comme quelque peu cachée, car je n'en avais jamais entendu parler pendant tout mon séjour à l'école, est l'alias d'espace de noms. Cela n'a pas été porté à mon attention jusqu'à ce que j'en trouve des exemples dans la documentation du boost. Bien sûr, maintenant que je le connais, vous pouvez le trouver dans n'importe quelle référence C ++ standard.
namespace fs = boost::filesystem;
fs::path myPath( strPath, fs::native );
using
.
Non seulement les variables peuvent être déclarées dans la partie init d'une for
boucle, mais aussi les classes et les fonctions.
for(struct { int a; float b; } loop = { 1, 2 }; ...; ...) {
...
}
Cela permet plusieurs variables de types différents.
L'opérateur de tableau est associatif.
A [8] est un synonyme de * (A + 8). Puisque l'addition est associative, cela peut être réécrit comme * (8 + A), qui est synonyme de ..... 8 [A]
Vous n'avez pas dit utile ... :-)
A
n'importe pas du tout. Par exemple, si A
était a char*
, le code serait toujours valide.
Une chose que l'on sait peu est que les unions peuvent aussi être des modèles:
template<typename From, typename To>
union union_cast {
From from;
To to;
union_cast(From from)
:from(from) { }
To getTo() const { return to; }
};
Et ils peuvent aussi avoir des constructeurs et des fonctions membres. Rien qui ait à voir avec l'héritage (y compris les fonctions virtuelles).
From
et To
sont définis et utilisés en conséquence. Une telle union peut être utilisée avec un comportement défini ( To
étant un tableau de caractères non signés ou une structure partageant une séquence initiale avec From
). Même si vous l'utilisez d'une manière non définie, cela peut toujours être utile pour les travaux de bas niveau. Quoi qu'il en soit, ce n'est qu'un exemple d'un modèle d'union - il peut y avoir d'autres utilisations pour une union basée sur un modèle.
C ++ est un standard, il ne devrait pas y avoir de fonctionnalités cachées ...
C ++ est un langage multi-paradigme, vous pouvez parier votre dernier argent sur l'existence de fonctionnalités cachées. Un exemple parmi tant d'autres: la métaprogrammation de modèles . Personne dans le comité de normalisation n'avait l'intention qu'il y ait un sous-langage complet de Turing qui soit exécuté au moment de la compilation.
Une autre fonctionnalité cachée qui ne fonctionne pas en C est la fonctionnalité de l' +
opérateur unaire . Vous pouvez l'utiliser pour promouvoir et décomposer toutes sortes de choses
+AnEnumeratorValue
Et votre valeur d'énumération qui avait auparavant son type d'énumération a maintenant le type entier parfait qui peut correspondre à sa valeur. Manuellement, vous ne sauriez guère ce type! Cela est nécessaire, par exemple, lorsque vous souhaitez implémenter un opérateur surchargé pour votre énumération.
Vous devez utiliser une classe qui utilise un initialiseur statique en classe sans définition hors classe, mais parfois il échoue à se lier? L'opérateur peut aider à créer un temporaire sans faire d'hypothèses ou de dépendances sur son type
struct Foo {
static int const value = 42;
};
// This does something interesting...
template<typename T>
void f(T const&);
int main() {
// fails to link - tries to get the address of "Foo::value"!
f(Foo::value);
// works - pass a temporary value
f(+Foo::value);
}
Voulez-vous passer deux pointeurs à une fonction, mais cela ne fonctionnera tout simplement pas? L'opérateur peut aider
// This does something interesting...
template<typename T>
void f(T const& a, T const& b);
int main() {
int a[2];
int b[3];
f(a, b); // won't work! different values for "T"!
f(+a, +b); // works! T is "int*" both time
}
La durée de vie des temporaires liées aux références const est celle que peu de gens connaissent. Ou du moins, c'est ma connaissance C ++ préférée que la plupart des gens ne connaissent pas.
const MyClass& x = MyClass(); // temporary exists as long as x is in scope
Une fonctionnalité intéressante qui n'est pas souvent utilisée est le bloc try-catch à l'échelle de la fonction:
int Function()
try
{
// do something here
return 42;
}
catch(...)
{
return -1;
}
L'utilisation principale serait de traduire l'exception en une autre classe d'exception et de la renvoyer, ou de traduire entre les exceptions et la gestion des codes d'erreur basée sur les retours.
return
partir du bloc catch de Function Try, mais uniquement de nouveau.
Beaucoup connaissent la métafonction identity
/ id
, mais il y a un bon cas d'utilisation pour les cas non-template: Facilité d'écriture des déclarations:
// void (*f)(); // same
id<void()>::type *f;
// void (*f(void(*p)()))(int); // same
id<void(int)>::type *f(id<void()>::type *p);
// int (*p)[2] = new int[10][2]; // same
id<int[2]>::type *p = new int[10][2];
// void (C::*p)(int) = 0; // same
id<void(int)>::type C::*p = 0;
Cela aide grandement à déchiffrer les déclarations C ++!
// boost::identity is pretty much the same
template<typename T>
struct id { typedef T type; };
template<typename Ret,typename... Args> using function = Ret (Args...); template<typename T> using pointer = *T;
-> pointer<function<void,int>> f(pointer<function<void,void>>);
or pointer<void(int)> f(pointer<void()>);
orfunction<pointer<function<void,int>>,pointer<function<void,void>>> f;
Une fonctionnalité assez cachée est que vous pouvez définir des variables dans une condition if, et sa portée ne s'étendra que sur les blocs if et else:
if(int * p = getPointer()) {
// do something
}
Certaines macros l'utilisent, par exemple pour fournir une portée "verrouillée" comme celle-ci:
struct MutexLocker {
MutexLocker(Mutex&);
~MutexLocker();
operator bool() const { return false; }
private:
Mutex &m;
};
#define locked(mutex) if(MutexLocker const& lock = MutexLocker(mutex)) {} else
void someCriticalPath() {
locked(myLocker) { /* ... */ }
}
BOOST_FOREACH l'utilise également sous le capot. Pour compléter cela, ce n'est pas seulement possible dans un if, mais aussi dans un switch:
switch(int value = getIt()) {
// ...
}
et dans une boucle while:
while(SomeThing t = getSomeThing()) {
// ...
}
(et aussi en condition pour). Mais je ne sais pas trop si tout cela est utile :)
if((a = f()) == b) ...
, mais cette réponse déclare en fait une variable dans la condition.
for(...; int i = foo(); ) ...;
Cela traversera le corps aussi longtemps que i
c'est vrai, en l'initialisant à chaque fois. La boucle que vous montrez montre simplement une déclaration de variable, mais pas une déclaration de variable qui agit simultanément comme une condition :)
Parfois, vous utilisez correctement l'opérateur virgule, mais vous voulez vous assurer qu'aucun opérateur virgule défini par l'utilisateur ne vous gêne, car par exemple, vous vous fiez aux points de séquence entre le côté gauche et droit ou vous voulez vous assurer que rien n'interfère avec le action. C'est là void()
qu'entre en jeu:
for(T i, j; can_continue(i, j); ++i, void(), ++j)
do_code(i, j);
Ignorez les espaces réservés que j'ai mis pour la condition et le code. L'important est le void()
, qui oblige le compilateur à utiliser l'opérateur virgule intégré. Cela peut être utile lors de l'implémentation de classes de traits, parfois aussi.
Initialisation du tableau dans le constructeur. Par exemple dans une classe si nous avons un tableau de int
as:
class clName
{
clName();
int a[10];
};
Nous pouvons initialiser tous les éléments du tableau à sa valeur par défaut (ici tous les éléments du tableau à zéro) dans le constructeur comme:
clName::clName() : a()
{
}
Oooh, je peux plutôt proposer une liste de détestations pour les animaux:
Du coté positif
Vous pouvez accéder aux données protégées et aux membres de fonction de n'importe quelle classe, sans comportement indéfini et avec la sémantique attendue. Lisez la suite pour voir comment. Lisez également le rapport de défaut à ce sujet.
Normalement, C ++ vous interdit d'accéder aux membres protégés non statiques de l'objet d'une classe, même si cette classe est votre classe de base
struct A {
protected:
int a;
};
struct B : A {
// error: can't access protected member
static int get(A &x) { return x.a; }
};
struct C : A { };
C'est interdit: vous et le compilateur ne savez pas sur quoi la référence pointe réellement. Il peut s'agir d'un C
objet, auquel cas la classe B
n'a aucune activité et aucune idée de ses données. Un tel accès n'est accordé que s'il x
s'agit d'une référence à une classe dérivée ou à une classe dérivée de celle-ci. Et cela pourrait permettre à un morceau de code arbitraire de lire n'importe quel membre protégé en créant simplement une classe "à jeter" qui lit les membres, par exemple std::stack
:
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
static std::deque<int> &get(std::stack<int> &s) {
// error: stack<int>::c is protected
return s.c;
}
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = pillager::get(s);
}
Comme vous le voyez, cela causerait bien trop de dégâts. Mais maintenant, les pointeurs membres permettent de contourner cette protection! Le point clé est que le type d'un pointeur de membre est lié à la classe qui contient réellement ledit membre - pas à la classe que vous avez spécifiée lors de la prise de l'adresse. Cela nous permet de contourner la vérification
struct A {
protected:
int a;
};
struct B : A {
// valid: *can* access protected member
static int get(A &x) { return x.*(&B::a); }
};
struct C : A { };
Et bien sûr, cela fonctionne également avec l' std::stack
exemple.
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
static std::deque<int> &get(std::stack<int> &s) {
return s.*(pillager::c);
}
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = pillager::get(s);
}
Cela sera encore plus facile avec une déclaration using dans la classe dérivée, qui rend le nom du membre public et fait référence au membre de la classe de base.
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
using std::stack<int>::c;
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = s.*(&pillager::c);
}
Une autre fonctionnalité cachée est que vous pouvez appeler des objets de classe qui peuvent être convertis en pointeurs de fonction ou en références. La résolution des surcharges se fait sur leur résultat et les arguments sont parfaitement transmis.
template<typename Func1, typename Func2>
class callable {
Func1 *m_f1;
Func2 *m_f2;
public:
callable(Func1 *f1, Func2 *f2):m_f1(f1), m_f2(f2) { }
operator Func1*() { return m_f1; }
operator Func2*() { return m_f2; }
};
void foo(int i) { std::cout << "foo: " << i << std::endl; }
void bar(long il) { std::cout << "bar: " << il << std::endl; }
int main() {
callable<void(int), void(long)> c(foo, bar);
c(42); // calls foo
c(42L); // calls bar
}
Celles-ci sont appelées "fonctions d'appel de substitution".
Fonctionnalités cachées:
Si une fonction lève une exception non répertoriée dans ses spécifications d'exception, mais que la fonction l'a std::bad_exception
dans sa spécification d'exception, l'exception est convertie std::bad_exception
et levée automatiquement. De cette façon, vous saurez au moins qu'un a bad_exception
été lancé. En savoir plus ici .
blocs d'essai de fonction
Le mot-clé template pour lever l'ambiguïté des typedefs dans un modèle de classe. Si le nom d'une spécialisation de modèle de membre apparaît après .
, ->
ou ::
opérateur, et ce nom a des paramètres de modèle explicitement qualifiés, préfixe le nom du modèle de membre avec le modèle mot - clé. En savoir plus ici .
les valeurs par défaut des paramètres de fonction peuvent être modifiées lors de l'exécution. En savoir plus ici .
A[i]
fonctionne aussi bien que i[A]
Les instances temporaires d'une classe peuvent être modifiées! Une fonction membre non-const peut être appelée sur un objet temporaire. Par exemple:
struct Bar {
void modify() {}
}
int main (void) {
Bar().modify(); /* non-const function invoked on a temporary. */
}
En savoir plus ici .
Si deux types différents sont présents avant et après le :
dans l' ?:
expression de l'opérateur ternary ( ), le type résultant de l'expression est celui qui est le plus général des deux. Par exemple:
void foo (int) {}
void foo (double) {}
struct X {
X (double d = 0.0) {}
};
void foo (X) {}
int main(void) {
int i = 1;
foo(i ? 0 : 0.0); // calls foo(double)
X x;
foo(i ? 0.0 : x); // calls foo(X)
}
map::operator[]
crée une entrée si la clé est manquante et renvoie une référence à la valeur d'entrée construite par défaut. Vous pouvez donc écrire:
map<int, string> m;
string& s = m[42]; // no need for map::find()
if (s.empty()) { // assuming we never store empty values in m
s.assign(...);
}
cout << s;
Je suis étonné de voir combien de programmeurs C ++ ne le savent pas.
.find()
.
const map::operator[]
génère des messages d'erreur"
Le fait de placer des fonctions ou des variables dans un espace de noms sans nom rend obsolète l'utilisation de static
pour les limiter à l'étendue du fichier.
static
la portée mondiale n'est en aucun cas obsolète. (Pour référence: C ++ 03 §D.2)
static
use ne doit être utilisé que dans un type de classe ou une fonction.
La définition des fonctions d'amis ordinaires dans les modèles de classe nécessite une attention particulière:
template <typename T>
class Creator {
friend void appear() { // a new function ::appear(), but it doesn't
… // exist until Creator is instantiated
}
};
Creator<void> miracle; // ::appear() is created at this point
Creator<double> oops; // ERROR: ::appear() is created a second time!
Dans cet exemple, deux instanciations différentes créent deux définitions identiques - une violation directe de l' ODR
Nous devons donc nous assurer que les paramètres du modèle du modèle de classe apparaissent dans le type de n'importe quelle fonction amie définie dans ce modèle (à moins que nous ne voulions empêcher plus d'une instanciation d'un modèle de classe dans un fichier particulier, mais c'est plutôt improbable). Appliquons ceci à une variante de notre exemple précédent:
template <typename T>
class Creator {
friend void feed(Creator<T>*){ // every T generates a different
… // function ::feed()
}
};
Creator<void> one; // generates ::feed(Creator<void>*)
Creator<double> two; // generates ::feed(Creator<double>*)
Clause de non-responsabilité: J'ai collé cette section à partir de modèles C ++: Le guide complet / Section 8.4
Peu connu, mais le code suivant convient
void f() { }
void g() { return f(); }
Aussi étrange que le suivant
void f() { return (void)"i'm discarded"; }
Sachant cela, vous pouvez en profiter dans certains domaines. Un exemple: les void
fonctions ne peuvent pas renvoyer une valeur, mais vous pouvez également ne pas simplement renvoyer rien, car elles peuvent être instanciées avec non-void. Au lieu de stocker la valeur dans une variable locale, ce qui provoquera une erreur pour void
, renvoyez simplement une valeur directement
template<typename T>
struct sample {
// assume f<T> may return void
T dosomething() { return f<T>(); }
// better than T t = f<T>(); /* ... */ return t; !
};
Lire un fichier dans un vecteur de chaînes:
vector<string> V;
copy(istream_iterator<string>(cin), istream_iterator<string>(),
back_inserter(V));
vector<string> V((istream_iterator<string>(cin)), istream_iterator<string>());
- parenthèses manquantes après le deuxième
Vous pouvez modéliser des champs de bits.
template <size_t X, size_t Y>
struct bitfield
{
char left : X;
char right : Y;
};
Je n'ai pas encore trouvé d'objectif pour cela, mais cela m'a vraiment surpris.
L'une des grammaires les plus intéressantes de tous les langages de programmation.
Trois de ces choses vont ensemble, et deux sont quelque chose de complètement différent ...
SomeType t = u;
SomeType t(u);
SomeType t();
SomeType t;
SomeType t(SomeType(u));
Tous sauf le troisième et le cinquième définissent un SomeType
objet sur la pile et l'initialisent (avec u
dans les deux premiers cas, et le constructeur par défaut dans le quatrième. Le troisième déclare une fonction qui ne prend aucun paramètre et renvoie a SomeType
. Le cinquième déclare de la même manière une fonction qui prend un paramètre par valeur de type SomeType
nommé u
.
Se débarrasser des déclarations anticipées:
struct global
{
void main()
{
a = 1;
b();
}
int a;
void b(){}
}
singleton;
Écrire des instructions de commutation avec les opérateurs?::
string result =
a==0 ? "zero" :
a==1 ? "one" :
a==2 ? "two" :
0;
Tout faire sur une seule ligne:
void a();
int b();
float c = (a(),b(),1.0f);
Remise à zéro des structures sans memset:
FStruct s = {0};
Normalisation / enroulement des valeurs d'angle et de temps:
int angle = (short)((+180+30)*65536/360) * 360/65536; //==-150
Attribution de références:
struct ref
{
int& r;
ref(int& r):r(r){}
};
int b;
ref a(b);
int c;
*(int**)&a = &c;
FStruct s = {};
est encore plus court.
main
? Je suggère global().main();
et simplement oublier le singleton ( vous pouvez simplement travailler avec le temporaire, qui obtient la vie de ce étendu )
L'opérateur conditionnel ternaire ?:
exige que ses deuxième et troisième opérandes aient des types "agréables" (parlant de manière informelle). Mais cette exigence a une exception (jeu de mots): le deuxième ou le troisième opérande peut être une expression throw (qui a un type void
), quel que soit le type de l'autre opérande.
En d'autres termes, on peut écrire les expressions C ++ parfaitement valides suivantes en utilisant l' ?:
opérateur
i = a > b ? a : throw something();
BTW, le fait que l'expression throw soit en fait une expression (de type void
) et non une instruction est une autre caractéristique peu connue du langage C ++. Cela signifie, entre autres, que le code suivant est parfaitement valide
void foo()
{
return throw something();
}
bien qu'il n'y ait pas grand chose à faire de cette façon (peut-être que dans un code de modèle générique, cela pourrait être utile).
La règle de dominance est utile, mais peu connue. Il dit que même si dans un chemin non unique à travers un treillis de classe de base, la recherche de nom pour un membre partiellement caché est unique si le membre appartient à une classe de base virtuelle:
struct A { void f() { } };
struct B : virtual A { void f() { cout << "B!"; } };
struct C : virtual A { };
// name-lookup sees B::f and A::f, but B::f dominates over A::f !
struct D : B, C { void g() { f(); } };
J'ai utilisé cela pour implémenter un support d'alignement qui détermine automatiquement l'alignement le plus strict au moyen de la règle de dominance.
Cela ne s'applique pas seulement aux fonctions virtuelles, mais également aux noms de typedef, aux membres statiques / non virtuels et à toute autre chose. Je l'ai vu utilisé pour implémenter des traits écrasables dans les méta-programmes.
struct C
dans votre exemple ...? À votre santé.