HOWTO · C++
Redefinición del tipo de clase en C++
Este artículo analiza cómo resolver la redefinición del tipo de clase en C++.
En esta página
La definición de una clase C++ establece la disposición de sus datos y declara sus miembros. Volver a definir esa clase en el mismo ámbito no es válido, por lo que el compilador informa de un error de redefinición de clase. Para corregirlo, hay que encontrar la definición duplicada y eliminarla o consolidarla. La herencia y la composición pueden servir para añadir comportamiento a un tipo distinto, pero ninguna permite definir dos veces la clase original.
Entender el error de redefinición de clases en C++
La definición de una clase introduce un tipo. Dentro de un mismo ámbito, un nombre de tipo no puede definirse dos veces como clase, aunque ambas definiciones tengan miembros idénticos. Por ejemplo, dos definiciones de class DelftStack { }; en la misma unidad de traducción producen un diagnóstico como redefinition of 'class DelftStack'. La redacción exacta y las ubicaciones señaladas varían según el compilador.
Esto es distinto de una declaración adelantada, como class DelftStack;, que anuncia un tipo sin definir sus miembros. Después de esa declaración sí se permite una definición completa. El error se refiere a definiciones repetidas, no a cada mención del nombre de una clase.
Encontrar definiciones duplicadas, incluso en los encabezados incluidos
Empieza por el nombre de la clase que aparece en el diagnóstico y revisa la línea indicada y la ubicación anterior que señale el compilador. Busca otra definición en el archivo fuente actual. Después, sigue sus directivas #include: un encabezado podría definir la misma clase, o dos encabezados incluidos podrían aportar cada uno una definición con ese nombre.
Si un encabezado se incluye más de una vez, su contenido puede repetirse en una unidad de traducción, salvo que use una guarda de inclusión o #pragma once. La guarda de inclusión convencional es la técnica portable del preprocesador; #pragma once es una directiva de compilador ampliamente compatible, pero no forma parte del estándar ISO C++. Ambas técnicas evitan que se vuelva a incluir ese mismo encabezado; ninguna resuelve el caso de dos encabezados distintos que definen la misma clase por separado. Comprueba también si un archivo fuente incluye un encabezado que define la clase y, además, repite el cuerpo de la clase.
Corregir el error conservando una sola definición
Elige una definición como fuente de verdad y elimina la copia accidental. Si ambas contienen miembros útiles, integra esos miembros en la única definición y actualiza los puntos de uso que haga falta. Si la definición pertenece a un encabezado, incluye ese encabezado y evita copiar el cuerpo de la clase en los archivos fuente. Añade una guarda de inclusión convencional o usa la directiva #pragma once, ampliamente compatible, para evitar que se procese el mismo encabezado varias veces.
Cambia el nombre de una clase solo si representa un tipo realmente distinto y con una función propia. Renombrar una copia accidental puede ocultar el problema de diseño o de inclusión y dejar el programa con dos tipos que compiten entre sí. Antes de cambiar nombres, confirma que el programa necesita ambos tipos y actualiza sus usos de forma coherente.
Ampliar una clase existente mediante herencia
Usa herencia pública cuando el nuevo tipo sea realmente un subtipo de la clase base y deba poder utilizarse mediante la interfaz pública de esa clase. Una clase derivada puede añadir miembros y funciones; no redefine la clase base. Con herencia pública, los miembros públicos de la base siguen siendo públicos y los protegidos siguen estando disponibles para el código de la clase derivada. La clase derivada no puede acceder directamente a los miembros privados de la base. La herencia privada y la protegida cambian la forma en que los miembros de la base quedan expuestos a quienes usan el tipo derivado.
Ejecuta este ejemplo autocontenido con un compilador de C++:
El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.
#include <iostream>
class Greeter {
public:
void greet() const { std::cout << "Hello from DelftStack!\n"; }
};
class FriendlyGreeter : public Greeter {
public:
void greet_twice() const {
greet();
greet();
}
};
int main() {
FriendlyGreeter greeter;
greeter.greet_twice();
}
Hello from DelftStack!
Hello from DelftStack!
Este programa escribe el saludo dos veces. FriendlyGreeter es una clase de saludo especializada y reutiliza la función pública heredada. Si esa relación de subtipo no es válida, la herencia puede dar lugar a una interfaz engañosa. Las etiquetas y el texto visible de la salida del ejemplo permanecen en inglés.
Añadir comportamiento mediante composición
La composición representa una relación «tiene un»: una clase almacena un objeto de otra clase y delega en él parte del trabajo. Así, la interfaz del tipo contenido se mantiene separada y la clase contenedora puede elegir qué operaciones exponer. En este ejemplo, Report almacena un miembro Formatter y delega en él la generación del formato; un informe tiene un formateador, pero no es un formateador. Las etiquetas y el texto visible dentro del ejemplo permanecen en inglés.
#include <string>
class Formatter {
public:
std::string format(const std::string& text) const {
return "Report: " + text;
}
};
class Report {
public:
std::string render(const std::string& text) const {
return formatter_.format(text);
}
private:
Formatter formatter_;
};
int main() {
Report report;
return report.render("Summary") == "Report: Summary" ? 0 : 1;
}
La composición también utiliza definiciones de clase distintas. Si la clase de un objeto miembro está definida en un encabezado, incluye ese encabezado y define la clase contenedora una sola vez. Añadir un objeto miembro no resuelve una definición duplicada en otro lugar del mismo ámbito.
Elegir entre herencia y composición para el cambio
Elige herencia cuando el nuevo tipo deba cumplir el contrato del tipo base y pueda usarse de forma segura en cualquier lugar donde se espere la base. Elige composición cuando el nuevo tipo contenga o utilice otro objeto como componente, o cuando deba exponer solo una parte de su comportamiento. La composición suele mantener más independientes las dependencias y las interfaces públicas; la herencia resulta útil cuando la sustitución es intencionada.
Ninguna de estas opciones corrige una definición de clase duplicada por accidente. Primero, asegúrate de que cada tipo tenga una única definición válida. Después, usa herencia o composición solo si la relación de diseño deseada lo justifica.
Verificar la corrección con un compilador
Después de eliminar o consolidar la definición duplicada, compila la unidad de traducción afectada y los destinos pertinentes del proyecto. Para un único archivo fuente llamado main.cpp, un comando habitual es g++ -std=c++17 main.cpp -o app; ejecuta el programa resultante con ./app. El ejemplo de herencia anterior se compiló y ejecutó: terminó con estado 0 y escribió las dos líneas de saludo mostradas arriba. El diagnóstico de definición duplicada es ilustrativo; la evidencia de ejecución suministrada corresponde al ejemplo de herencia, no a una ejecución del caso de diagnóstico.
Si el diagnóstico persiste, vuelve a revisar las dos ubicaciones señaladas por el compilador y rastrea de nuevo los encabezados incluidos. Si el duplicado aparece solo en una configuración de compilación, compara sus rutas de inclusión y definiciones del preprocesador. Al compartir el diagnóstico, conserva el texto como contenido seleccionable, ya que la redacción y los números de línea dependen del compilador y de la compilación.