C++
Comportamiento indefinido
Buscar..
Introducción
¿Qué es el comportamiento indefinido (UB)? De acuerdo con la norma ISO C ++ (§1.3.24, N4296), es un "comportamiento por el que esta norma internacional no impone requisitos".
Esto significa que cuando un programa se encuentra con UB, se le permite hacer lo que quiera. A menudo, esto significa un choque, pero puede que simplemente no haga nada, haga que los demonios salgan volando por tu nariz , ¡o incluso parece que funciona correctamente!
No hace falta decir que debes evitar escribir código que invoque a UB.
Observaciones
Si un programa contiene un comportamiento indefinido, el estándar de C ++ no impone restricciones a su comportamiento.
- Puede parecer que funciona según lo previsto por el desarrollador, pero también puede fallar o producir resultados extraños.
- El comportamiento puede variar entre ejecuciones del mismo programa.
- Cualquier parte del programa puede funcionar mal, incluidas las líneas que vienen antes de la línea que contiene un comportamiento indefinido.
- La implementación no es necesaria para documentar el resultado de un comportamiento indefinido.
Una implementación puede documentar el resultado de una operación que produce un comportamiento indefinido de acuerdo con el estándar, pero un programa que depende de dicho comportamiento documentado no es portátil.
¿Por qué existe un comportamiento indefinido?
Intuitivamente, el comportamiento indefinido se considera algo malo, ya que tales errores no pueden manejarse con amabilidad mediante, por ejemplo, controladores de excepciones.
Pero dejar un comportamiento indefinido es en realidad una parte integral de la promesa de C ++ "no pagas por lo que no usas". El comportamiento indefinido permite que un compilador asuma que el desarrollador sabe lo que está haciendo y no introduce código para verificar los errores resaltados en los ejemplos anteriores.
Encontrar y evitar comportamientos indefinidos.
Algunas herramientas se pueden usar para descubrir un comportamiento indefinido durante el desarrollo:
- La mayoría de los compiladores tienen marcas de advertencia para advertir sobre algunos casos de comportamiento indefinido en tiempo de compilación.
- Las versiones más recientes de gcc y clang incluyen un indicador denominado "Desinfectante de comportamiento indefinido" (
-fsanitize=undefined
) que verificará el comportamiento indefinido en el tiempo de ejecución, a un costo de rendimiento. -
lint
herramientas similares alint
pueden realizar un análisis de comportamiento indefinido más completo.
Comportamiento indefinido, no especificado y definido por la implementación
De la sección 1.9 (Ejecución del programa) de la norma C ++ 14 (ISO / IEC 14882: 2014):
Las descripciones semánticas en esta Norma Internacional definen una máquina abstracta no determinista parametrizada. [CORTAR]
Ciertos aspectos y operaciones de la máquina abstracta se describen en esta Norma Internacional como definidos por la implementación (por ejemplo,
sizeof(int)
). Estos constituyen los parámetros de la máquina abstracta . Cada implementación deberá incluir documentación que describa sus características y comportamiento en estos aspectos. [CORTAR]Ciertos otros aspectos y operaciones de la máquina abstracta se describen en esta Norma Internacional como no especificados (por ejemplo, evaluación de expresiones en un nuevo inicializador si la función de asignación no puede asignar memoria). Donde sea posible, esta norma internacional define un conjunto de comportamientos permitidos. Estos definen los aspectos no deterministas de la máquina abstracta. Una instancia de la máquina abstracta puede tener más de una ejecución posible para un programa dado y una entrada dada.
Ciertas otras operaciones se describen en esta Norma Internacional como indefinidas (o ejemplo, el efecto de intentar modificar un objeto
const
). [ Nota : esta Norma Internacional no impone requisitos sobre el comportamiento de los programas que contienen un comportamiento indefinido. - nota final ]
Leer o escribir a través de un puntero nulo.
int *ptr = nullptr;
*ptr = 1; // Undefined behavior
Este es un comportamiento indefinido , porque un puntero nulo no apunta a ningún objeto válido, por lo que no hay ningún objeto en *ptr
para escribir.
Aunque esto causa con mayor frecuencia un fallo de segmentación, no está definido y puede pasar cualquier cosa.
No hay declaración de retorno para una función con un tipo de retorno no nulo
Omitir la declaración de return
en una función que tiene un tipo de retorno que no es void
es un comportamiento indefinido .
int function() {
// Missing return statement
}
int main() {
function(); //Undefined Behavior
}
La mayoría de los compiladores de hoy en día emiten una advertencia en el momento de la compilación para este tipo de comportamiento indefinido.
Nota: main
es la única excepción a la regla. Si main
no tiene una declaración de return
, el compilador inserta automáticamente return 0;
Para ti, para que puedas dejarlo fuera de forma segura.
Modificar un literal de cadena
char *str = "hello world";
str[0] = 'H';
"hello world"
es una cadena literal, por lo que modificarlo da un comportamiento indefinido.
La inicialización de str
en el ejemplo anterior fue obsoleta formalmente (programada para su eliminación de una versión futura del estándar) en C ++ 03. Varios compiladores antes de 2003 podrían emitir una advertencia sobre esto (por ejemplo, una conversión sospechosa). Después de 2003, los compiladores suelen advertir sobre una conversión obsoleta.
El ejemplo anterior es ilegal y da como resultado un diagnóstico del compilador, en C ++ 11 y versiones posteriores. Se puede construir un ejemplo similar para mostrar un comportamiento indefinido permitiendo explícitamente la conversión de tipo, como:
char *str = const_cast<char *>("hello world");
str[0] = 'H';
Accediendo a un índice fuera de límites
Es un comportamiento indefinido acceder a un índice que está fuera de los límites de una matriz (o el contenedor de la biblioteca estándar para esa materia, ya que todos se implementan utilizando una matriz sin procesar ):
int array[] = {1, 2, 3, 4, 5};
array[5] = 0; // Undefined behavior
Se permite tener un puntero que apunta al final de la matriz (en este caso, array + 5
), simplemente no se puede eliminar la referencia, ya que no es un elemento válido.
const int *end = array + 5; // Pointer to one past the last index
for (int *p = array; p != end; ++p)
// Do something with `p`
En general, no se le permite crear un puntero fuera de límites. Un puntero debe apuntar a un elemento dentro de la matriz, o uno más allá del final.
División entera por cero
int x = 5 / 0; // Undefined behavior
La división por 0
está definida matemáticamente y, como tal, tiene sentido que se trate de un comportamiento indefinido.
Sin embargo:
float x = 5.0f / 0.0f; // x is +infinity
La mayoría de la implementación implementa IEEE-754, que define la división de punto flotante por cero para devolver NaN
(si el numerador es 0.0f
), el infinity
(si el numerador es positivo) o el -infinity
(si el numerador es negativo).
Desbordamiento de enteros firmado
int x = INT_MAX + 1;
// x can be anything -> Undefined behavior
Si durante la evaluación de una expresión, el resultado no está definido matemáticamente o no está en el rango de valores representables para su tipo, el comportamiento no está definido.
(C ++ 11 párrafo 5/4 estándar)
Este es uno de los más desagradables, ya que por lo general produce un comportamiento reproducible y sin interrupciones, por lo que los desarrolladores pueden verse tentados a confiar en gran medida en el comportamiento observado.
Por otra parte:
unsigned int x = UINT_MAX + 1;
// x is 0
está bien definido ya que:
Los enteros sin signo, declarados sin firmar, obedecerán las leyes del módulo aritmético
2^n
donden
es el número de bits en la representación del valor de ese tamaño particular de entero.
(C ++ 11 párrafo 3.9.1 / 4)
A veces los compiladores pueden explotar un comportamiento indefinido y optimizar
signed int x ;
if(x > x + 1)
{
//do something
}
Aquí, dado que no se define un desbordamiento de enteros con signo, el compilador es libre de asumir que nunca puede suceder y, por lo tanto, puede optimizar el bloque "if"
Usando una variable local sin inicializar
int a;
std::cout << a; // Undefined behavior!
Esto da como resultado un comportamiento indefinido , porque a
no está inicializado.
A menudo, incorrectamente, se afirma que esto se debe a que el valor es "indeterminado", o "cualquier valor que haya en esa ubicación de memoria antes". Sin embargo, es el hecho de acceder al valor de a
en el ejemplo anterior lo que da un comportamiento indefinido. En la práctica, la impresión de un "valor de basura" es un síntoma común en este caso, pero esa es solo una forma posible de comportamiento indefinido.
Aunque es muy poco probable en la práctica (ya que depende del soporte de hardware específico) el compilador podría electrocutar al programador al compilar el ejemplo de código anterior. Con un compilador y un soporte de hardware de este tipo, tal respuesta al comportamiento indefinido aumentaría notablemente el entendimiento promedio (vivo) del programador del verdadero significado del comportamiento indefinido, que es que el estándar no impone ninguna restricción al comportamiento resultante.
El uso de un valor indeterminado de tipo de unsigned char
no produce un comportamiento indefinido si el valor se utiliza como:
- el segundo o tercer operando del operador condicional ternario;
- el operando derecho del operador de coma incorporado;
- el operando de una conversión a caracteres
unsigned char
; - el operando derecho del operador de asignación, si el operando izquierdo también es de tipo
unsigned char
; - el inicializador para un objeto
unsigned char
;
o si el valor es descartado. En tales casos, el valor indeterminado simplemente se propaga al resultado de la expresión, si corresponde.
Tenga en cuenta que una variable static
siempre se inicializa con cero (si es posible):
static int a;
std::cout << a; // Defined behavior, 'a' is 0
Múltiples definiciones no idénticas (la regla de una definición)
Si una clase, enumeración, función en línea, plantilla o miembro de una plantilla tiene un enlace externo y se define en múltiples unidades de traducción, todas las definiciones deben ser idénticas o el comportamiento no está definido según la Regla de una definición (ODR) .
foo.h
:
class Foo {
public:
double x;
private:
int y;
};
Foo get_foo();
foo.cpp
:
#include "foo.h"
Foo get_foo() { /* implementation */ }
main.cpp
:
// I want access to the private member, so I am going to replace Foo with my own type
class Foo {
public:
double x;
int y;
};
Foo get_foo(); // declare this function ourselves since we aren't including foo.h
int main() {
Foo foo = get_foo();
// do something with foo.y
}
El programa anterior muestra un comportamiento indefinido porque contiene dos definiciones de la clase ::Foo
, que tiene un enlace externo, en diferentes unidades de traducción, pero las dos definiciones no son idénticas. A diferencia de la redefinición de una clase dentro de la misma unidad de traducción, este compilador no requiere que este problema sea diagnosticado.
Emparejamiento incorrecto de la asignación de memoria y desasignación
Un objeto solo puede ser desasignado por delete
si fue asignado por new
y no es una matriz. Si el argumento para delete
no fue devuelto por new
o es una matriz, el comportamiento no está definido.
Un objeto solo puede ser desasignado por delete[]
si fue asignado por new
y es una matriz. Si el argumento para delete[]
no fue devuelto por new
o no es una matriz, el comportamiento no está definido.
Si el argumento para free
no fue devuelto por malloc
, el comportamiento es indefinido.
int* p1 = new int;
delete p1; // correct
// delete[] p1; // undefined
// free(p1); // undefined
int* p2 = new int[10];
delete[] p2; // correct
// delete p2; // undefined
// free(p2); // undefined
int* p3 = static_cast<int*>(malloc(sizeof(int)));
free(p3); // correct
// delete p3; // undefined
// delete[] p3; // undefined
Dichos problemas se pueden evitar evitando completamente malloc
y programas free
en C ++, prefiriendo los punteros inteligentes de la biblioteca estándar sobre new
y delete
en bruto, y prefiriendo std::vector
y std::string
sobre new
y delete[]
.
Accediendo a un objeto como el tipo equivocado
En la mayoría de los casos, es ilegal acceder a un objeto de un tipo como si fuera un tipo diferente (sin tener en cuenta los calificadores cv). Ejemplo:
float x = 42;
int y = reinterpret_cast<int&>(x);
El resultado es un comportamiento indefinido.
Hay algunas excepciones a esta regla estricta de aliasing :
- Se puede acceder a un objeto de tipo de clase como si fuera de un tipo que es una clase base del tipo de clase real.
- Se puede acceder a cualquier tipo como
char
ounsigned char
, pero lo contrario no es cierto: no se puede acceder a una matriz char como si fuera un tipo arbitrario. - Se puede acceder a un tipo entero con signo como el tipo sin signo correspondiente y viceversa .
Una regla relacionada es que si se llama a una función miembro no estática en un objeto que en realidad no tiene el mismo tipo que la clase que define la función, o una clase derivada, entonces ocurre un comportamiento indefinido. Esto es cierto incluso si la función no accede al objeto.
struct Base {
};
struct Derived : Base {
void f() {}
};
struct Unrelated {};
Unrelated u;
Derived& r1 = reinterpret_cast<Derived&>(u); // ok
r1.f(); // UB
Base b;
Derived& r2 = reinterpret_cast<Derived&>(b); // ok
r2.f(); // UB
Desbordamiento de punto flotante
Si una operación aritmética que produce un tipo de punto flotante produce un valor que no está en el rango de valores representables del tipo de resultado, el comportamiento no está definido de acuerdo con el estándar C ++, pero puede definirse por otros estándares que la máquina pueda cumplir. tales como IEEE 754.
float x = 1.0;
for (int i = 0; i < 10000; i++) {
x *= 10.0; // will probably overflow eventually; undefined behavior
}
Llamando (Puro) a los Miembros Virtuales del Constructor o Destructor
La Norma (10.4) establece:
Las funciones miembro se pueden llamar desde un constructor (o destructor) de una clase abstracta; el efecto de hacer una llamada virtual (10.3) a una función virtual pura directa o indirectamente para el objeto que se está creando (o destruyendo) desde tal constructor (o destructor) no está definido.
De manera más general, algunas autoridades de C ++, por ejemplo, Scott Meyers, sugieren nunca llamar a funciones virtuales (incluso no puras) de constructores y constructores.
Considere el siguiente ejemplo, modificado desde el enlace anterior:
class transaction
{
public:
transaction(){ log_it(); }
virtual void log_it() const = 0;
};
class sell_transaction : public transaction
{
public:
virtual void log_it() const { /* Do something */ }
};
Supongamos que creamos un objeto sell_transaction
:
sell_transaction s;
Esto implícitamente llama al constructor de sell_transaction
, que primero llama al constructor de la transaction
. Sin embargo, cuando se llama al constructor de la transaction
, el objeto todavía no es del tipo sell_transaction
, sino más bien del tipo transaction
.
En consecuencia, la llamada en transaction::transaction()
a log_it
, no hará lo que pueda parecer algo intuitivo, es decir, call sell_transaction::log_it
.
Si
log_it
es puramente virtual, como en este ejemplo, el comportamiento no está definido.Si
log_it
no es virtual puro, setransaction::log_it
.
Eliminar un objeto derivado a través de un puntero a una clase base que no tiene un destructor virtual.
class base { };
class derived: public base { };
int main() {
base* p = new derived();
delete p; // The is undefined behavior!
}
En la sección [expr.delete] §5.3.5 / 3, el estándar dice que si se llama a delete
en un objeto cuyo tipo estático no tiene un destructor virtual
:
Si el tipo estático del objeto que se va a eliminar es diferente de su tipo dinámico, el tipo estático será una clase base del tipo dinámico del objeto que se eliminará y el tipo estático tendrá un destructor virtual o el comportamiento es indefinido.
Este es el caso, independientemente de la pregunta sobre si la clase derivada agregó algún miembro de datos a la clase base.
Accediendo a una referencia colgante
Es ilegal acceder a una referencia a un objeto que ha quedado fuera del alcance o ha sido destruido. Se dice que dicha referencia está colgando ya que ya no se refiere a un objeto válido.
#include <iostream>
int& getX() {
int x = 42;
return x;
}
int main() {
int& r = getX();
std::cout << r << "\n";
}
En este ejemplo, la variable local x
queda fuera del alcance cuando retorna getX
. (Tenga en cuenta que la extensión de vida útil no puede extender la vida útil de una variable local más allá del alcance del bloque en el que está definida). Por lo tanto, r
es una referencia pendiente. Este programa tiene un comportamiento indefinido, aunque puede parecer que funciona e imprime 42
en algunos casos.
Extendiendo el espacio de nombres `std` o` posix`
El estándar (17.6.4.2.1 / 1) generalmente prohíbe extender el std
nombres estándar :
El comportamiento de un programa en C ++ no está definido si agrega declaraciones o definiciones al espacio de nombres estándar o a un espacio de nombres dentro del espacio de nombres estándar, a menos que se especifique lo contrario.
Lo mismo ocurre con posix
(17.6.4.2.2 / 1):
El comportamiento de un programa de C ++ no está definido si agrega declaraciones o definiciones al espacio de nombres posix o a un espacio de nombres dentro del espacio de nombres posix a menos que se especifique lo contrario.
Considera lo siguiente:
#include <algorithm>
namespace std
{
int foo(){}
}
Nada en el estándar prohíbe el algorithm
(o uno de los encabezados que incluye) la definición de la misma definición, por lo que este código violaría la Regla de una definición .
Entonces, en general, esto está prohibido. Sin embargo, hay excepciones específicas permitidas . Tal vez lo más útil es que está permitido agregar especializaciones para los tipos definidos por el usuario. Entonces, por ejemplo, supongamos que su código tiene
class foo
{
// Stuff
};
Entonces lo siguiente está bien
namespace std
{
template<>
struct hash<foo>
{
public:
size_t operator()(const foo &f) const;
};
}
Desbordamiento durante la conversión hacia o desde el tipo de punto flotante
Si, durante la conversión de:
- un tipo entero a un tipo de punto flotante,
- un tipo de punto flotante a un tipo entero, o
- un tipo de punto flotante a un tipo de punto flotante más corto,
El valor de origen está fuera del rango de valores que se pueden representar en el tipo de destino, el resultado es un comportamiento indefinido. Ejemplo:
double x = 1e100;
int y = x; // int probably cannot hold numbers that large, so this is UB
Conversión estática de base a derivada no válida
Si se utiliza static_cast
para convertir un puntero (referencia de referencia) en una clase base en un puntero (referencia de referencia) en una clase derivada, pero el operando no apunta (referido de referencia) a un objeto del tipo de clase derivada, el comportamiento es indefinido. Ver Base a conversión derivada .
Función de llamada a través del tipo de puntero de función no coincidente
Para llamar a una función mediante un puntero de función, el tipo de puntero de función debe coincidir exactamente con el tipo de función. De lo contrario, el comportamiento es indefinido. Ejemplo:
int f();
void (*p)() = reinterpret_cast<void(*)()>(f);
p(); // undefined
Modificar un objeto const
Cualquier intento de modificar un objeto const
produce un comportamiento indefinido. Esto se aplica a las variables const
, miembros de objetos const
y miembros de clase declarados const
. (Sin embargo, un mutable
miembro de un const
objeto no es const
.)
Tal intento se puede hacer a través de const_cast
:
const int x = 123;
const_cast<int&>(x) = 456;
std::cout << x << '\n';
Un compilador usualmente alineará el valor de un objeto const int
, por lo que es posible que este código compile e imprima 123
. Los compiladores también pueden colocar valores const
objetos en la memoria de solo lectura, por lo que puede ocurrir una falla de segmentación. En cualquier caso, el comportamiento no está definido y el programa puede hacer cualquier cosa.
El siguiente programa oculta un error mucho más sutil:
#include <iostream>
class Foo* instance;
class Foo {
public:
int get_x() const { return m_x; }
void set_x(int x) { m_x = x; }
private:
Foo(int x, Foo*& this_ref): m_x(x) {
this_ref = this;
}
int m_x;
friend const Foo& getFoo();
};
const Foo& getFoo() {
static const Foo foo(123, instance);
return foo;
}
void do_evil(int x) {
instance->set_x(x);
}
int main() {
const Foo& foo = getFoo();
do_evil(456);
std::cout << foo.get_x() << '\n';
}
En este código, getFoo
crea un singleton de tipo const Foo
y su miembro m_x
se inicializa en 123
. Luego se llama do_evil
y el valor de foo.m_x
aparentemente se cambia a 456. ¿Qué salió mal?
A pesar de su nombre, do_evil
no hace nada particularmente malo; todo lo que hace es llamar a un setter a través de un Foo*
. Pero ese puntero apunta a un objeto const Foo
aunque no se usó const_cast
. Este puntero se obtuvo a través del constructor de Foo
. Un objeto const
no se convierte en const
hasta que se completa su inicialización, por this
tiene el tipo Foo*
, no const Foo*
, dentro del constructor.
Por lo tanto, el comportamiento indefinido ocurre aunque no hay construcciones obviamente peligrosas en este programa.
Acceso a miembro inexistente a través de puntero a miembro
Cuando se accede a un miembro no estático de un objeto a través de un puntero a miembro, si el objeto no contiene realmente al miembro indicado por el puntero, el comportamiento no está definido. (Este puntero a miembro se puede obtener a través de static_cast
).
struct Base { int x; };
struct Derived : Base { int y; };
int Derived::*pdy = &Derived::y;
int Base::*pby = static_cast<int Base::*>(pdy);
Base* b1 = new Derived;
b1->*pby = 42; // ok; sets y in Derived object to 42
Base* b2 = new Base;
b2->*pby = 42; // undefined; there is no y member in Base
Conversión derivada a base no válida para punteros a miembros
Cuando static_cast
se usa para convertir TD::*
a TB::*
, el miembro apuntado debe pertenecer a una clase que sea una clase base o una clase derivada de B
De lo contrario el comportamiento es indefinido. Consulte Conversión derivada a base para punteros a miembros.
Aritmética de puntero no válido
Los siguientes usos de la aritmética de punteros provocan un comportamiento indefinido:
Suma o resta de un entero, si el resultado no pertenece al mismo objeto de matriz que el operando de puntero. (Aquí, se considera que el elemento uno más allá del final todavía pertenece a la matriz.)
int a[10]; int* p1 = &a[5]; int* p2 = p1 + 4; // ok; p2 points to a[9] int* p3 = p1 + 5; // ok; p2 points to one past the end of a int* p4 = p1 + 6; // UB int* p5 = p1 - 5; // ok; p2 points to a[0] int* p6 = p1 - 6; // UB int* p7 = p3 - 5; // ok; p7 points to a[5]
Resta dos punteros si ambos no pertenecen al mismo objeto de matriz. (De nuevo, se considera que el elemento uno más allá del final pertenece a la matriz.) La excepción es que se pueden restar dos punteros nulos, lo que arroja 0.
int a[10]; int b[10]; int *p1 = &a[8], *p2 = &a[3]; int d1 = p1 - p2; // yields 5 int *p3 = p1 + 2; // ok; p3 points to one past the end of a int d2 = p3 - p2; // yields 7 int *p4 = &b[0]; int d3 = p4 - p1; // UB
Resta dos punteros si el resultado se desborda
std::ptrdiff_t
.Cualquier aritmética de punteros en la que el tipo de punto del operando no coincida con el tipo dinámico del objeto apuntado (ignorando la calificación cv). De acuerdo con el estándar, "[en] en particular, un puntero a una clase base no se puede usar para la aritmética de punteros cuando la matriz contiene objetos de un tipo de clase derivada".
struct Base { int x; }; struct Derived : Base { int y; }; Derived a[10]; Base* p1 = &a[1]; // ok Base* p2 = p1 + 1; // UB; p1 points to Derived Base* p3 = p1 - 1; // likewise Base* p4 = &a[2]; // ok auto p5 = p4 - p1; // UB; p4 and p1 point to Derived const Derived* p6 = &a[1]; const Derived* p7 = p6 + 1; // ok; cv-qualifiers don't matter
Desplazando por un número de posiciones no válido
Para el operador de cambio incorporado, el operando derecho debe ser no negativo y estrictamente menor que el ancho de bits del operando izquierdo promovido. De lo contrario, el comportamiento es indefinido.
const int a = 42;
const int b = a << -1; // UB
const int c = a << 0; // ok
const int d = a << 32; // UB if int is 32 bits or less
const int e = a >> 32; // also UB if int is 32 bits or less
const signed char f = 'x';
const int g = f << 10; // ok even if signed char is 10 bits or less;
// int must be at least 16 bits
Volviendo de una función [[noreturn]]
Ejemplo de la Norma, [dcl.attr.noreturn]:
[[ noreturn ]] void f() {
throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
if (i > 0)
throw "positive";
}
Destruyendo un objeto que ya ha sido destruido.
En este ejemplo, se invoca explícitamente un destructor para un objeto que luego se destruirá automáticamente.
struct S {
~S() { std::cout << "destroying S\n"; }
};
int main() {
S s;
s.~S();
} // UB: s destroyed a second time here
Se produce un problema similar cuando se std::unique_ptr<T>
un std::unique_ptr<T>
para apuntar a una T
con duración de almacenamiento automática o estática.
void f(std::unique_ptr<S> p);
int main() {
S s;
std::unique_ptr<S> p(&s);
f(std::move(p)); // s destroyed upon return from f
} // UB: s destroyed
Otra forma de destruir un objeto dos veces es tener dos shared_ptr
gestionen el objeto sin compartir la propiedad entre ellos.
void f(std::shared_ptr<S> p1, std::shared_ptr<S> p2);
int main() {
S* p = new S;
// I want to pass the same object twice...
std::shared_ptr<S> sp1(p);
std::shared_ptr<S> sp2(p);
f(sp1, sp2);
} // UB: both sp1 and sp2 will destroy s separately
// NB: this is correct:
// std::shared_ptr<S> sp(p);
// f(sp, sp);
Recursión de plantilla infinita
Ejemplo de la Norma, [temp.inst] / 17:
template<class T> class X {
X<T>* p; // OK
X<T*> a; // implicit generation of X<T> requires
// the implicit instantiation of X<T*> which requires
// the implicit instantiation of X<T**> which ...
};