Características ocultas de C ++

¿No te encanta C ++ cuando se trata de la línea de preguntas “características ocultas”? Pensé que lo tiraría allí. ¿Cuáles son algunas de las características ocultas de C ++?

La mayoría de los progtwigdores de C ++ están familiarizados con el operador ternario:

x = (y < 0) ? 10 : 20; 

Sin embargo, no se dan cuenta de que se puede usar como valor l:

 (a == 0 ? a : b) = 1; 

que es la abreviatura de

 if (a == 0) a = 1; else b = 1; 

Usar con precaución 🙂

Puede poner URI en la fuente de C ++ sin error. Por ejemplo:

 void foo() { http://stackoverflow.com/ int bar = 4; ... } 

Puntero aritmética.

Los progtwigdores de C ++ prefieren evitar los punteros debido a los errores que pueden introducirse.

¿El C ++ más genial que he visto? Literales analógicos.

Estoy de acuerdo con la mayoría de las publicaciones allí: C ++ es un lenguaje multi-paradigma, por lo que las características “ocultas” que encontrará (aparte de “comportamientos indefinidos” que debe evitar a toda costa) son usos inteligentes de las instalaciones.

La mayoría de esas instalaciones no son características incorporadas del lenguaje, sino que se basan en bibliotecas.

El más importante es el RAII , a menudo ignorado durante años por los desarrolladores de C ++ que vienen del mundo C. La sobrecarga del operador a menudo es una característica incomprendida que permite el comportamiento similar a un array (operador de subíndices), operaciones tipo puntero (punteros inteligentes) y operaciones tipo build-in (matrices de multiplicación).

El uso de la excepción es a menudo difícil, pero con cierto trabajo, puede producir código realmente sólido a través de especificaciones de seguridad excepcionales (incluido el código que no fallará, o que tendrá características de compromiso que tendrán éxito, o volverá a funcionar). su estado original).

La característica más famosa de “oculto” de C ++ es la metaprogtwigción de plantillas , ya que le permite tener su progtwig parcialmente (o totalmente) ejecutado en tiempo de comstackción en lugar de en tiempo de ejecución. Sin embargo, esto es difícil y debes tener una comprensión sólida de las plantillas antes de intentarlo.

Otros hacen uso del paradigma múltiple para producir “formas de progtwigción” fuera del antepasado de C ++, es decir, C.

Mediante el uso de funtores , puede simular funciones, con el tipo adicional de seguridad y ser con estado. Usando el patrón de comando , puede retrasar la ejecución del código. La mayoría de los otros patrones de diseño se pueden implementar de manera fácil y eficiente en C ++ para producir estilos de encoding alternativos que no deberían estar dentro de la lista de “paradigmas oficiales de C ++”.

Mediante el uso de plantillas , puede producir código que funcionará en la mayoría de los tipos, incluido el que pensó al principio. También puede boost la seguridad del tipo (como un tipo automatizado seguro malloc / realloc / free). Las características del objeto C ++ son realmente potentes (y por lo tanto, peligrosas si se usan descuidadamente), pero incluso el polymorphism dynamic tiene su versión estática en C ++: el CRTP .

He encontrado que la mayoría de los libros de tipo ” Effective C ++ ” de Scott Meyers o ” Exceptional C ++ ” de Herb Sutter son fáciles de leer y un tesoro de información sobre las características conocidas y menos conocidas de C ++.

Entre mi preferido es uno que debería hacer que el pelo de cualquier progtwigdor Java se eleve del horror: en C ++, la forma más orientada a objetos para agregar una función a un objeto es a través de una función no miembro no amigo, en lugar de un miembro. función (es decir, método de clase), porque:

  • En C ++, una interfaz de clase es a la vez sus funciones miembro y las funciones no miembro en el mismo espacio de nombres

  • Las funciones no miembro de no amigos no tienen acceso privilegiado a la clase interna. Como tal, el uso de una función miembro sobre un no amigo no miembro debilitará la encapsulación de la clase.

Esto nunca deja de sorprender incluso a los desarrolladores experimentados.

(Fuente: Entre otros, el Gurú de la semana # 84 de Herb Sutter: http://www.gotw.ca/gotw/084.htm )

Una característica del lenguaje que considero que está un tanto oculta, porque nunca había escuchado sobre eso durante todo mi tiempo en la escuela, es el alias del espacio de nombres. No me llamó la atención hasta que encontré ejemplos de esto en la documentación de impulso. Por supuesto, ahora que lo sé, puedes encontrarlo en cualquier referencia estándar de C ++.

 namespace fs = boost::filesystem; fs::path myPath( strPath, fs::native ); 

No solo se pueden declarar variables en la parte init de un ciclo for , sino también clases y funciones.

 for(struct { int a; float b; } loop = { 1, 2 }; ...; ...) { ... } 

Eso permite múltiples variables de diferentes tipos.

El operador de la matriz es asociativo.

A [8] es un sinónimo de * (A + 8). Como la sum es asociativa, puede reescribirse como * (8 + A), que es un sinónimo de ….. 8 [A]

No dijiste útil … 🙂

Una cosa que se sabe poco es que los sindicatos también pueden ser plantillas:

 template union union_cast { From from; To to; union_cast(From from) :from(from) { } To getTo() const { return to; } }; 

Y también pueden tener constructores y funciones de miembros. Nada que tenga que ver con la herencia (incluidas las funciones virtuales).

C ++ es un estándar, no debería haber funciones ocultas …

C ++ es un lenguaje multi-paradigma, puede apostar su último dinero en que hay funciones ocultas. Un ejemplo de muchos: metaprogtwigción de plantillas . Nadie en el comité de estándares pretendía que hubiera una sublenguaje completa de Turing que se ejecutara en tiempo de comstackción.

Otra característica oculta que no funciona en C es la funcionalidad del operador unario + . Puedes usarlo para promocionar y decaer toda clase de cosas

Convertir una enumeración a un número entero

 +AnEnumeratorValue 

Y el valor del enumerador que anteriormente tenía su tipo de enumeración ahora tiene el tipo de entero perfecto que puede ajustarse a su valor. ¡Manualmente, difícilmente sabrías ese tipo! Esto es necesario, por ejemplo, cuando desea implementar un operador sobrecargado para su enumeración.

Obtenga el valor de una variable

Tienes que usar una clase que utiliza un inicializador estático en su clase sin una definición fuera de clase, pero a veces no se puede vincular? El operador puede ayudar a crear un temporal sin hacer suposiciones o dependencias en su tipo

 struct Foo { static int const value = 42; }; // This does something interesting... template 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); } 

Decayar una matriz a un puntero

¿Quieres pasar dos punteros a una función, pero simplemente no va a funcionar? El operador puede ayudar

 // This does something interesting... template 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 vida de los temporales relacionados con las referencias es una que pocas personas conocen. O al menos es mi conocimiento favorito de C ++ que la mayoría de las personas desconoce.

 const MyClass& x = MyClass(); // temporary exists as long as x is in scope 

Una buena característica que no se usa a menudo es el bloque Try-catch en toda la función:

 int Function() try { // do something here return 42; } catch(...) { return -1; } 

El uso principal sería traducir la excepción a otra clase de excepción y volver a lanzar, o traducir entre excepciones y el manejo de código de error basado en el retorno.

Muchos conocen la metafunción identity / id , pero hay un buen caso de uso para casos que no son de plantilla: declaraciones de facilidad de redacción:

 // void (*f)(); // same id::type *f; // void (*f(void(*p)()))(int); // same id::type *f(id::type *p); // int (*p)[2] = new int[10][2]; // same id::type *p = new int[10][2]; // void (C::*p)(int) = 0; // same id::type C::*p = 0; 

¡Ayuda a descifrar las declaraciones de C ++ en gran medida!

 // boost::identity is pretty much the same template struct id { typedef T type; }; 

Una característica bastante oculta es que puede definir variables dentro de una condición if, y su scope se extenderá solo sobre if y sus bloques else:

 if(int * p = getPointer()) { // do something } 

Algunas macros usan eso, por ejemplo para proporcionar un scope “bloqueado” como este:

 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) { /* ... */ } } 

También BOOST_FOREACH lo usa debajo del capó. Para completar esto, no solo es posible en un if, sino también en un switch:

 switch(int value = getIt()) { // ... } 

y en un ciclo while:

 while(SomeThing t = getSomeThing()) { // ... } 

(y también en una condición for) Pero no estoy muy seguro de si todo esto es útil 🙂

Evitar que el operador de coma llame a las sobrecargas del operador

A veces haces un uso válido del operador de coma, pero quieres asegurarte de que ningún operador de coma definido por el usuario se meta en el camino, porque por ejemplo dependes de puntos de secuencia entre el lado izquierdo y derecho o quieres asegurarte de que nada interfiera con el deseado acción. Aquí es donde entra en juego void() :

 for(T i, j; can_continue(i, j); ++i, void(), ++j) do_code(i, j); 

Ignora los marcadores de posición que puse para la condición y el código. Lo que es importante es el void() , que hace que el comstackdor fuerce a usar el operador de comas incorporado. Esto puede ser útil al implementar clases de rasgos, a veces también.

Array de inicialización en constructor. Por ejemplo, en una clase si tenemos una matriz de int como:

 class clName { clName(); int a[10]; }; 

Podemos inicializar todos los elementos en la matriz a su valor predeterminado (aquí todos los elementos de la matriz a cero) en el constructor como:

 clName::clName() : a() { } 

Oooh, puedo hacer una lista de odio a las mascotas en su lugar:

  • Los destructores deben ser virtuales si tiene la intención de utilizar polimórficamente
  • A veces, los miembros se inicializan por defecto, a veces no son
  • Las clases locales no se pueden usar como parámetros de plantilla (los hacen menos útiles)
  • especificadores de excepciones: parecen útiles, pero no son
  • las sobrecargas de función ocultan las funciones de la clase base con diferentes firmas.
  • ninguna estandarización útil en la internacionalización (juego de caracteres portátil estándar, ¿alguien? Tendremos que esperar hasta C ++ 0x)

En el lado positivo

  • característica oculta: función prueba bloques. Desafortunadamente no he encontrado un uso para eso. Sí, sé por qué lo agregaron, pero tienes que volver a lanzarlo en un constructor que lo hace inútil.
  • Vale la pena mirar detenidamente las garantías de STL sobre la validez del iterador después de la modificación del contenedor, que puede permitirle hacer algunos bucles ligeramente más agradables.
  • Boost: no es un secreto, pero vale la pena usarlo.
  • Optimización del valor de retorno (no es obvio, pero está específicamente permitido por el estándar)
  • Functores también conocidos como objetos de función aka operator (). Esto es usado ampliamente por el STL. no es realmente un secreto, pero es un efecto secundario ingenioso de la sobrecarga del operador y las plantillas.

Puede acceder a datos protegidos y miembros de funciones de cualquier clase, sin un comportamiento indefinido y con semántica esperada. Sigue leyendo para ver cómo. Lea también el informe de defectos sobre esto.

Normalmente, C ++ le prohíbe acceder a miembros protegidos no estáticos del objeto de una clase, incluso si esa clase es su clase base

 struct A { protected: int a; }; struct B : A { // error: can't access protected member static int get(A &x) { return xa; } }; struct C : A { }; 

Eso está prohibido: tú y el comstackdor no saben a qué apunta la referencia. Podría ser un objeto C , en cuyo caso la clase B no tiene ningún negocio ni pista sobre sus datos. Tal acceso solo se concede si x es una referencia a una clase derivada o derivada de ella. Y podría permitir que un fragmento arbitrario de código lea cualquier miembro protegido inventando una clase “descartable” que lea los miembros, por ejemplo de std::stack :

 void f(std::stack &s) { // now, let's decide to mess with that stack! struct pillager : std::stack { static std::deque &get(std::stack &s) { // error: stack::c is protected return sc; } }; // haha, now let's inspect the stack's middle elements! std::deque &d = pillager::get(s); } 

Seguramente, como ves, esto causaría demasiado daño. ¡Pero ahora, los indicadores de miembros permiten eludir esta protección! El punto clave es que el tipo de puntero de miembro está vinculado a la clase que realmente contiene dicho miembro, no a la clase que especificó al tomar la dirección. Esto nos permite evadir el control

 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 { }; 

Y, por supuesto, también funciona con el ejemplo de std::stack .

 void f(std::stack &s) { // now, let's decide to mess with that stack! struct pillager : std::stack { static std::deque &get(std::stack &s) { return s.*(pillager::c); } }; // haha, now let's inspect the stack's middle elements! std::deque &d = pillager::get(s); } 

Eso será aún más fácil con una statement using en la clase derivada, lo que hace que el nombre del miembro sea público y se refiere al miembro de la clase base.

 void f(std::stack &s) { // now, let's decide to mess with that stack! struct pillager : std::stack { using std::stack::c; }; // haha, now let's inspect the stack's middle elements! std::deque &d = s.*(&pillager::c); } 

Otra característica oculta es que puede llamar objetos de clase que se pueden convertir a punteros a funciones o referencias. La resolución de sobrecarga se realiza sobre el resultado de ellos, y los argumentos se envían perfectamente.

 template 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 c(foo, bar); c(42); // calls foo c(42L); // calls bar } 

Estas se llaman “funciones de llamada sustituta”.

Funciones ocultas:

  1. Las funciones virtuales puras pueden tener implementación. Ejemplo común, destructor virtual puro.
  2. Si una función arroja una excepción que no figura en sus especificaciones de excepción, pero la función tiene std::bad_exception en su especificación de excepción, la excepción se convierte en std::bad_exception y se lanza automáticamente. De esa forma, al menos sabrá que se bad_exception una bad_exception . Lea más aquí .

  3. función probar bloques

  4. La palabra clave de la plantilla en la desambiguación de typedefs en una plantilla de clase. Si aparece el nombre de una especialización de plantilla de miembro después de a . , -> o :: operador, y ese nombre tiene parámetros de plantilla explícitamente calificados, prefija el nombre de la plantilla de miembro con la plantilla de palabra clave. Lea más aquí .

  5. los parámetros predeterminados de función pueden cambiarse en tiempo de ejecución. Lea más aquí .

  6. A[i] funciona tan bien como i[A]

  7. ¡Se pueden modificar instancias temporales de una clase! Una función miembro no const puede ser invocada en un objeto temporal. Por ejemplo:

     struct Bar { void modify() {} } int main (void) { Bar().modify(); /* non-const function invoked on a temporary. */ } 

    Lea más aquí .

  8. Si dos tipos diferentes están presentes antes y después de : en la expresión de operador ternario ( ?: :), Entonces el tipo resultante de la expresión es el que es el más general de los dos. Por ejemplo:

     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[] crea la entrada si falta la clave y devuelve la referencia al valor de entrada construido por defecto. Entonces puedes escribir:

 map 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; 

Estoy sorprendido de cuántos progtwigdores de C ++ no saben esto.

Putting functions or variables in a nameless namespace deprecates the use of static to restrict them to file scope.

Defining ordinary friend functions in class templates needs special attention:

 template  class Creator { friend void appear() { // a new function ::appear(), but it doesn't … // exist until Creator is instantiated } }; Creator miracle; // ::appear() is created at this point Creator oops; // ERROR: ::appear() is created a second time! 

In this example, two different instantiations create two identical definitions—a direct violation of the ODR

We must therefore make sure the template parameters of the class template appear in the type of any friend function defined in that template (unless we want to prevent more than one instantiation of a class template in a particular file, but this is rather unlikely). Let’s apply this to a variation of our previous example:

 template  class Creator { friend void feed(Creator*){ // every T generates a different … // function ::feed() } }; Creator one; // generates ::feed(Creator*) Creator two; // generates ::feed(Creator*) 

Disclaimer: I have pasted this section from C++ Templates: The Complete Guide / Section 8.4

void functions can return void values

Little known, but the following code is fine

 void f() { } void g() { return f(); } 

Aswell as the following weird looking one

 void f() { return (void)"i'm discarded"; } 

Knowing about this, you can take advantage in some areas. One example: void functions can’t return a value but you can also not just return nothing, because they may be instantiated with non-void. Instead of storing the value into a local variable, which will cause an error for void , just return a value directly

 template struct sample { // assume f may return void T dosomething() { return f(); } // better than T t = f(); /* ... */ return t; ! }; 

Read a file into a vector of strings:

  vector V; copy(istream_iterator(cin), istream_iterator(), back_inserter(V)); 

istream_iterator

You can template bitfields.

 template  struct bitfield { char left : X; char right : Y; }; 

I have yet to come up with any purpose for this, but it sure as heck surprised me.

One of the most interesting grammars of any programming languages.

Three of these things belong together, and two are something altogether different…

 SomeType t = u; SomeType t(u); SomeType t(); SomeType t; SomeType t(SomeType(u)); 

All but the third and fifth define a SomeType object on the stack and initialize it (with u in the first two case, and the default constructor in the fourth. The third is declaring a function that takes no parameters and returns a SomeType . The fifth is similarly declaring a function that takes one parameter by value of type SomeType named u .

Getting rid of forward declarations:

 struct global { void main() { a = 1; b(); } int a; void b(){} } singleton; 

Writing switch-statements with ?: operators:

 string result = a==0 ? "zero" : a==1 ? "one" : a==2 ? "two" : 0; 

Doing everything on a single line:

 void a(); int b(); float c = (a(),b(),1.0f); 

Zeroing structs without memset:

 FStruct s = {0}; 

Normalizing/wrapping angle- and time-values:

 int angle = (short)((+180+30)*65536/360) * 360/65536; //==-150 

Assigning references:

 struct ref { int& r; ref(int& r):r(r){} }; int b; ref a(b); int c; *(int**)&a = &c; 

The ternary conditional operator ?: requires its second and third operand to have “agreeable” types (speaking informally). But this requirement has one exception (pun intended): either the second or third operand can be a throw expression (which has type void ), regardless of the type of the other operand.

In other words, one can write the following pefrectly valid C++ expressions using the ?: operator

 i = a > b ? a : throw something(); 

BTW, the fact that throw expression is actually an expression (of type void ) and not a statement is another little-known feature of C++ language. This means, among other things, that the following code is perfectly valid

 void foo() { return throw something(); } 

although there’s not much point in doing it this way (maybe in some generic template code this might come handy).

The dominance rule is useful, but little known. It says that even if in a non-unique path through a base-class lattice, name-lookup for a partially hidden member is unique if the member belongs to a virtual base-class:

 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(); } }; 

I've used this to implement alignment-support that automatically figures out the strictest alignment by means of the dominance rule.

This does not only apply to virtual functions, but also to typedef names, static/non-virtual members and anything else. I've seen it used to implement overwritable traits in meta-programs.