sintaxis - tipos de datos en c++
Construcciones de C++ reemplazando constructos de C (21)
Macros vs. plantillas en línea
Estilo C:
#define max(x,y) (x) > (y) ? (x) : (y)
Estilo C ++
inline template<typename T>
const T& max(const T& x, const T& y)
{
return x > y ? x : y;
}
Motivo para preferir el enfoque de C ++:
- Tipo de seguridad: exige que los argumentos sean del mismo tipo
- Los errores de sintaxis en la definición de max apuntarán al lugar correcto, en lugar de a donde llama la macro
- Puede depurar en la función
Después de discutir con un desarrollador recién llegado en mi equipo, me di cuenta de que todavía hay, en C ++, hábitos de usar constructos C porque se supone que son mejores (es decir, más rápidos, más delgados, más bonitos, elige tu razón).
¿Cuáles son los ejemplos que vale la pena compartir, mostrando una construcción C, en comparación con la construcción C ++ similar?
Para cada ejemplo, necesito leer las razones por las cuales la construcción C ++ es tan buena o incluso mejor que la construcción C original. El objetivo es ofrecer alternativas a algunos constructos C que se consideran algo peligrosos / inseguros en el código C ++ (C ++ 0x solo se aceptan respuestas válidas siempre que se marque claramente como C ++ 0x solamente).
A continuación, publicaré una respuesta (inicialización estructural de struct) como ejemplo.
Nota 1: Por favor, una respuesta por caso. Si tiene múltiples casos, publique múltiples respuestas
Nota 2: Esta no es una pregunta en C. No agregue la etiqueta "C" a esta pregunta. Se supone que esto no debe convertirse en una lucha entre C ++ y C. Solo el estudio de algunos constructos del subconjunto C de C ++, y su alternativa en otros "kits de herramientas" de C ++.
Nota 3: Esta no es una pregunta de bashing en C. Quiero razones. Se suavizarán las comparaciones de jactancia, agresión y no probada. Mencionar las características de C ++ sin un equivalente en C podría considerarse fuera del tema: quiero colocar una característica C lado a lado contra una característica C ++.
inicialización struct inline vs. constructores en línea
A veces, necesitamos en C ++ una simple agregación de datos. Los datos son algo independientes, protegerlo a través de la encapsulación no valdría la pena el esfuerzo.
// C-like code in C++
struct CRect
{
int x ;
int y ;
} ;
void doSomething()
{
CRect r0 ; // uninitialized
CRect r1 = { 25, 40 } ; // vulnerable to some silent struct reordering,
// or adding a parameter
}
; Veo tres problemas con el código de arriba:
- si el objeto no se inicializa específicamente, no se inicializará todo
- si cambiamos xoy (por cualquier razón), la inicialización C predeterminada en doSomething () será incorrecta
- si agregamos el miembro az, y nos gustó que fuera "cero" por defecto, aún tendríamos que cambiar cada inicialización en línea
El código siguiente tendrá los constructores en línea (si es realmente útil), y por lo tanto, tendrá un costo cero (como el código C anterior):
// C++
struct CRect
{
CRect() : x(0), y(0) {} ;
CRect(int X, int Y) : x(X), y(Y) {} ;
int x ;
int y ;
} ;
void doSomething()
{
CRect r0 ;
CRect r1(25, 40) ;
}
(La ventaja es que podríamos agregar un operador == métodos, pero este bono está fuera de tema, y vale la pena mencionarlo, pero no vale la pena como respuesta).
Editar: C99 ha llamado inicializado
Adam Rosenfield hizo un comentario interesante que encuentro muy interesante:
C99 permite inicializadores con nombre: CRect r = {.x = 25, yy = 40}
Esto no se compilará en C ++. Supongo que esto debería agregarse a C ++, aunque solo sea para la compatibilidad C. De todos modos, en C, alivia el problema mencionado en esta respuesta.
#define vs. const
Sigo viendo códigos como este de desarrolladores que han codificado C por mucho tiempo:
#define MYBUFSIZE 256
. . .
char somestring[MYBUFSIZE];
etcétera etcétera.
En C ++ esto sería mejor como:
const int MYBUFSIZE = 256;
char somestring[MYBUFSIZE];
Por supuesto, aún mejor sería que un desarrollador use std :: string en lugar de una matriz de caracteres, pero eso es un problema aparte.
Los problemas con las macros de C son innumerables, sin que la comprobación de tipos sea el principal problema en este caso.
Por lo que he visto, este parece ser un hábito extremadamente difícil para los programadores de C que se convierten a C ++ para romperse.
Casi cualquier uso de void*
.
iostream vs stdio.h
Cª:
#include <stdio.h>
int main()
{
int num = 42;
printf("%s%d%c", "Hello World/n", num, ''/n'');
return 0;
}
La cadena de formato se analiza en tiempo de ejecución, lo que significa que no es segura.
en C ++:
#include <iostream>
int main()
{
int num = 42;
std::cout << "Hello World/n" << num << ''/n'';
}
Los tipos de datos son conocidos en tiempo de compilación, y también hay menos para escribir porque no es necesaria una cadena de formato.
Función qsort
C contra plantilla de función de sort
C ++ ''. Este último ofrece seguridad tipo a través de plantillas que tienen consecuencias obvias y menos obvias:
- La seguridad de tipo hace que el código sea menos propenso a errores.
- La interfaz de
sort
es un poco más fácil (no es necesario especificar el tamaño de los elementos). - El compilador conoce el tipo de la función del comparador. Si, en lugar de un puntero de función, el usuario pasa un objeto de función, la
sort
tendrá un rendimiento más rápido queqsort
porque la comparación se vuelve trivial. Este no es el caso con los punteros de función que son necesarios en la versión C.
El siguiente ejemplo demuestra el uso de qsort
versus sort
en una matriz de sort
C de int
.
int pint_less_than(void const* pa, void const* pb) {
return *static_cast<int const*>(pa) - *static_cast<int const*>(pb);
}
struct greater_than {
bool operator ()(int a, int b) {
return a > b;
}
};
template <std::size_t Size>
void print(int (&arr)[Size]) {
std::copy(arr, arr + Size, std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
}
int main() {
std::size_t const size = 5;
int values[] = { 4, 3, 6, 8, 2 };
{ // qsort
int arr[size];
std::copy(values, values + size, arr);
std::qsort(arr, size, sizeof(int), &pint_less_than);
print(arr);
}
{ // sort
int arr[size];
std::copy(values, values + size, arr);
std::sort(arr, arr + size);
print(arr);
}
{ // sort with custom comparer
int arr[size];
std::copy(values, values + size, arr);
std::sort(arr, arr + size, greater_than());
print(arr);
}
}
iostreams
Las E / S formateadas pueden ser más rápidas usando el tiempo de ejecución de C. Pero no creo que las E / S de bajo nivel (lectura, escritura, etc.) sean más lentas con las transmisiones. La capacidad de leer o escribir en un flujo sin importar si el otro extremo es un archivo, una cadena, un zócalo o algún objeto definido por el usuario es increíblemente útil.
En c, gran parte de su funcionalidad dinámica se logra al pasar sobre los indicadores de función. C ++ le permite tener objetos de función, proporcionando una mayor flexibilidad y seguridad. Presentaré un ejemplo adaptado del excelente conocimiento común de C ++ de Stephen Dewhurst
C Punteros a la función:
int fibonacci() {
static int a0 = 0, a1 =1; // problematic....
int temp = a0;
a0 = a1;
a1 = temp + a0;
return temp;
}
void Graph( (int)(*func)(void) );
void Graph2( (int)(*func1)(void), (int)(*func2)(void) );
Graph(fibonacci);
Graph2(fibonacci,fibonacci);
Puede ver que, dadas las variables estáticas en la función fibonacci()
, el orden de ejecución de Graph
y Graph2()
cambiará el comportamiento, a pesar de que la llamada a Graph2()
puede tener resultados inesperados ya que cada llamada a func1
y func2
arrojará el siguiente valor en la serie, no el siguiente valor en una instancia individual de la serie con respecto a la función a la que se llama. (Obviamente, podría externalizar el estado de la función, pero eso no tendría sentido, sin mencionar que confundiría al usuario y complicaría las funciones del cliente)
Objetos de función C ++:
class Fib {
public:
Fib() : a0_(1), a1_(1) {}
int operator();
private:
int a0_, a1_;
};
int Fib::operator() {
int temp = a0_;
a0_ = a1_;
a1_ = temp + a0_;
return temp;
}
template <class FuncT>
void Graph( FuncT &func );
template <class FuncT>
void Graph2( FuncT &func1, FuncT &func2);
Fib a,b,c;
Graph(a);
Graph2(b,c);
Aquí, el orden de ejecución de las funciones Graph()
y Graph2()
no cambia el resultado de la llamada. Además, en la llamada a Graph2()
c
mantienen un estado separado a medida que se utilizan; cada uno generará la secuencia completa de Fibonacci individualmente.
Ofreceré algo que quizás sea abundantemente obvio, Namespaces.
el alcance global abarrotado de c:
void PrintToScreen(const char *pBuffer);
void PrintToFile(const char *pBuffer);
void PrintToSocket(const char *pBuffer);
void PrintPrettyToScreen(const char *pBuffer);
vs.
las subdivisiones definibles de C ++ del alcance global, los espacios de nombres:
namespace Screen
{
void Print(const char *pBuffer);
}
namespace File
{
void Print(const char *pBuffer);
}
namespace Socket
{
void Print(const char *pBuffer);
}
namespace PrettyScreen
{
void Print(const char *pBuffer);
}
Este es un pequeño ejemplo inventado, pero la capacidad de clasificar tokens que defina en ámbitos que tengan sentido evita el propósito confuso de la función con el contexto en el que se llama.
nuevo en C ++ vs malloc en C. (para gestión de memoria)
el nuevo operador permite llamar a los constructores de clase mientras que malloc no.
Parámetros predeterminados:
DO:
void AddUser(LPCSTR lpcstrName, int iAge, const char *lpcstrAddress);
void AddUserByNameOnly(LPCSTR lpcstrName)
{
AddUser(lpcstrName, -1,NULL);
}
Reemplazo de C ++ / equivalente:
void User::Add(LPCSTR lpcstrName, int iAge=-1, const char *lpcstrAddress=NULL);
Por qué es una mejora:
Permite al programador escribir la función express del programa en menos líneas de código fuente y en una forma más compacta. También permite que los valores por defecto para los parámetros no utilizados se expresen más cerca de donde realmente se usan. Para quien llama, simplifica la interfaz a la clase / estructura.
RAII y toda la gloria resultante frente a la adquisición / liberación manual de recursos
Cª:
Resource r;
r = Acquire(...);
... Code that uses r ...
Release(r);
donde como ejemplos, Resource
podría ser un puntero a la memoria y Acquire / Release asignará / liberará esa memoria, o podría ser un descriptor de archivo abierto donde Acquire / Release abrirá / cerrará ese archivo.
Esto presenta una serie de problemas:
- Es posible que olvides llamar a
Release
- El código no transmite información sobre el flujo de datos para
r
. Sir
se adquiere y se libera dentro del mismo ámbito, el código no se auto-documenta. - Durante el tiempo entre el
Resource r
y elResource r
r.Acquire(...)
,r
es realmente accesible, a pesar de no estar inicializado. Esta es una fuente de errores.
Aplicando la metodología RAII (Adquisición de recursos es inicialización), en C ++ obtenemos
class ResourceRAII
{
Resource rawResource;
public:
ResourceRAII(...) {rawResource = Acquire(...);}
~ResourceRAII() {Release(rawResource);}
// Functions for manipulating the resource
};
...
{
ResourceRAII r(...);
... Code that uses r ...
}
La versión de C ++ se asegurará de que no se olvide de liberar el recurso (si lo hace, tiene una pérdida de memoria, que se detecta más fácilmente mediante herramientas de depuración). Obliga al programador a ser explícito sobre cómo fluye el flujo de datos del recurso (es decir, si solo existe durante el alcance de una función, esto se dejaría en claro mediante una construcción de ResourceRAII en la pila). No hay ningún punto durante el tiempo entre la creación del objeto de recurso y su destrucción cuando el recurso no es válido.
¡También es una excepción segura!
Arrays dinámicos vs. contenedores STL
Estilo C:
int **foo = new int*[n];
for (int x = 0; x < n; ++x) foo[x] = new int[m];
// (...)
for (int x = 0; x < n; ++x) delete[] foo[x];
delete[] foo;
C ++ - estilo:
std::vector< std::vector<int> > foo(n, std::vector<int>(m));
// (...)
Por qué los contenedores STL son mejores:
- Son redimensionables, las matrices tienen un tamaño fijo
- Son excepciones seguras: si se produce una excepción no controlada en (...) parte, entonces la memoria de matriz puede tener fugas: el contenedor se crea en la pila, por lo que se destruirá correctamente durante el desenrollado
- Implementan verificación encuadernada, por ejemplo, vector :: at () (salir de los límites de la matriz generará una Infracción de acceso y terminará el programa)
- Son más fáciles de usar, por ejemplo, vector :: clear () vs. limpieza manual de la matriz
- Ocultan detalles de administración de memoria, lo que hace que el código sea más legible
Declaración de variables locales (automáticas)
(No es cierto desde C99, como señaló correctamente Jonathan Leffler)
En C, debe declarar todas las variables locales al comienzo del bloque en el que están definidas.
En C ++ es posible (y preferible) posponer la definición de variable antes de que deba ser utilizada. Más tarde es preferible por dos razones principales:
- Aumenta la claridad del programa (como se ve el tipo de variable donde se usa por primera vez).
- Hace la refactorización más fácil (ya que tiene pequeños fragmentos cohesivos de código).
- Mejora la eficiencia del programa (ya que las variables se construyen justo cuando realmente las necesita).
std::copy
vs. memcpy
Primero hay preocupaciones de usabilidad:
-
memcpy
toma punteros vacíos. Esto arroja seguridad tipo. -
std::copy
permite la superposición de rangos en ciertos casos (constd::copy_backward
existente para otros casos superpuestos), mientras quememcpy
no lo permite. -
memcpy
solo funciona en punteros, mientras questd::copy
funciona en iteradores (de los cuales los punteros son un caso especial, por lo questd::copy
funciona en los punteros). Esto significa que puede, por ejemplo,std::copy
elementos en unastd::list
.
Sin duda, toda esta seguridad y generalidad adicional tiene un precio, ¿verdad?
Cuando memcpy
, encontré que std::copy
tenía una ligera ventaja de rendimiento sobre memcpy
.
En otras palabras, parece que no hay ninguna razón para usar memcpy
en el código C ++ real.
Después de la publicación de Fizzer en constructos C ++ reemplazando constructos C , escribiré aquí mi respuesta:
Advertencia: la solución de C ++ propuesta a continuación no es C ++ estándar, sino que es una extensión de g ++ y Visual C ++, y se propone como estándar para C ++ 0x (gracias a los comentarios de Fizzer sobre esto)
Tenga en cuenta que la respuesta de Johannes Schaub - litb ofrece otra forma compatible con C ++ 03 para hacerlo de todos modos.
Pregunta
Cómo extraer el tamaño de una matriz C?
Propuesta de solución C
Fuente: ¿ Cuándo son beneficiosas las macros C ++?
#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])
A diferencia de la solución de plantilla ''preferida'' discutida en un hilo actual, puede usarla como una expresión constante:
char src[23];
int dest[ARRAY_SIZE(src)];
No estoy de acuerdo con Fizzer ya que hay una solución de plantilla capaz de generar una expresión constante (de hecho, una parte muy interesante de las plantillas es su capacidad para generar expresiones constantes en la compilación)
De todos modos, ARRAY_SIZE es una macro capaz de extraer el tamaño de una matriz C. No detallaré las macros en C ++: el objetivo es encontrar una solución de C ++ igual o mejor.
Una mejor solución de C ++?
La siguiente versión de C ++ no tiene ninguno de los problemas de macro, y puede hacer cualquier cosa de la misma manera:
template <typename T, size_t size>
inline size_t array_size(T (&p)[size])
{
// return sizeof(p)/sizeof(p[0]) ;
return size ; // corrected after Konrad Rudolph''s comment.
}
demostración
Como lo demuestra el siguiente código:
#include <iostream>
// C-like macro
#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])
// C++ replacement
template <typename T, size_t size>
inline size_t array_size(T (&p)[size])
{
// return sizeof(p)/sizeof(p[0]) ;
return size ; // corrected after Konrad Rudolph''s comment.
}
int main(int argc, char **argv)
{
char src[23];
char * src2 = new char[23] ;
int dest[ARRAY_SIZE(src)];
int dest2[array_size(src)];
std::cout << "ARRAY_SIZE(src) : " << ARRAY_SIZE(src) << std::endl ;
std::cout << "array_size(src) : " << array_size(src) << std::endl ;
std::cout << "ARRAY_SIZE(src2) : " << ARRAY_SIZE(src2) << std::endl ;
// The next line won''t compile
//std::cout << "array_size(src2) : " << array_size(src2) << std::endl ;
return 0;
}
Esto dará como resultado:
ARRAY_SIZE(src) : 23
array_size(src) : 23
ARRAY_SIZE(src2) : 4
En el código anterior, la macro confundió un puntero para una matriz, y por lo tanto, devolvió un valor incorrecto (4, en lugar de 23). La plantilla, en cambio, se negó a compilar:
/main.cpp|539|error: no matching function for call to ‘array_size(char*&)’|
Demostrando así que la solución de plantilla es: * capaz de generar expresión constante en tiempo de compilación * capaz de detener la compilación si se utiliza de forma incorrecta
Conclusión
Por lo tanto, en general, los argumentos para la plantilla son:
- no hay contaminación del código tipo macro
- se puede ocultar dentro de un espacio de nombres
- puede proteger de una evaluación de tipo incorrecta (un puntero a la memoria no es una matriz)
Nota: Gracias por la implementación de Microsoft de strcpy_s para C ++ ... sabía que esto me serviría algún día ... ^ _ ^
http://msdn.microsoft.com/en-us/library/td1esda9.aspx
Editar: la solución es una extensión estandarizada para C ++ 0x
Fizzer hizo correctamente la observación de que esto no era válido en el estándar actual de C ++, y era bastante cierto (como pude verificar en g ++ con la opción -pedantic activada).
Aún así, no solo esto se puede usar hoy en dos compiladores principales (es decir, Visual C ++ y g ++), pero esto se consideró para C ++ 0x, como se propone en los siguientes borradores:
- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1521.pdf (ver secciones "2.1 Funciones de expresión constante")
- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2691.pdf (ver secciones "5.19 Expresiones constantes" y "7.1.5 El especificador constexpr")
El único cambio para C ++ 0x es probablemente algo así como:
inline template <typename T, size_t size>
constexpr size_t array_size(T (&p)[size])
{
//return sizeof(p)/sizeof(p[0]) ;
return size ; // corrected after Konrad Rudolph''s comment.
}
(tenga en cuenta la palabra clave constexpr )
Editar 2
Johannes Schaub - La respuesta de litb ofrece otra forma compatible con C ++ 03 para hacerlo. Copiaré pegue la fuente aquí para referencia, pero visite su respuesta para un ejemplo completo ( y upmod it! ):
template<typename T, size_t N> char (& array_size(T(&)[N]) )[N];
Que se usa como:
int p[] = { 1, 2, 3, 4, 5, 6 };
int u[sizeof array_size(p)]; // we get the size (6) at compile time.
Muchas neuronas en mi cerebro se frieron para hacerme entender la naturaleza de array_size
(pista: es una función que devuelve una referencia a una matriz de N caracteres).
:-)
En aras del equilibrio, esta publicación tiene un ejemplo de una construcción de estilo C que a veces es mejor que el equivalente de estilo de C ++.
En respuesta a Alex Che , y en justicia a C:
En C99, las especificaciones estándar ISO actuales para las variables C, pueden declararse en cualquier lugar de un bloque, al igual que en C ++. El siguiente código es válido C99:
int main(void)
{
for(int i = 0; i < 10; i++)
...
int r = 0;
return r;
}
Siguiendo la construcción de paercebal usando matrices de longitud variable para evitar la limitación de que las funciones aún no pueden devolver expresiones constantes, aquí hay una manera de hacerlo, de otra manera:
template<typename T, size_t N> char (& array_size(T(&)[N]) )[N];
Lo he escrito en algunas de mis otras respuestas, pero no encaja en ningún otro lugar mejor que en este hilo. Ahora, bueno, así es como uno podría usarlo:
void pass(int *q) {
int n1 = sizeof(q); // oops, size of the pointer!
int n2 = sizeof array_size(q); // error! q is not an array!
}
int main() {
int p[] = { 1, 2, 3, 4, 5, 6 };
int u[sizeof array_size(p)]; // we get the size at compile time.
pass(p);
}
Ventaja sobre sizeof
- Falla para no-arrays. No funcionará silenciosamente para los punteros
- Dirá en el código que se toma el tamaño de la matriz.
Funciones sobrecargadas:
DO:
AddUserName(int userid, NameInfo nameinfo);
AddUserAge(int userid, int iAge);
AddUserAddress(int userid, AddressInfo addressinfo);
C ++ equivalente / reemplazo:
User::AddInfo(NameInfo nameinfo);
User::AddInfo(int iAge);
User::AddInfo(AddressInfo addressInfo);
Por qué es una mejora:
Permite al programador expresar la interfaz de manera que el concepto de la función se exprese en el nombre, y el tipo de parámetro solo se expresa en el parámetro en sí. Permite que la persona que llama interactúe con la clase de una manera más cercana a una expresión de los conceptos. También generalmente resulta en un código fuente más conciso, compacto y legible.