Argumento predeterminado vs sobrecargas en C ++

Por ejemplo, en lugar de

void shared_ptr::reset() noexcept; template  void shared_ptr::reset(Y* ptr); 

uno puede pensar

 template  void shared_ptr::reset(Y* ptr = nullptr); 

Creo que la diferencia de rendimiento es despreciable aquí, y la segunda versión es más concisa. ¿Hay alguna razón específica por la cual el estándar de C ++ sea el primero?

Se ha formulado la misma pregunta para el idioma Kotlin, y se prefiere el argumento predeterminado allí.

Actualizar:

std::unique_ptr::reset() sigue el diseño del argumento predeterminado (vea aquí ). Así que creo que la razón por la cual std::shared_ptr::reset() usa sobrecargas es porque tienen diferentes especificaciones de excepción.

La diferencia crucial es que, de hecho, las dos operaciones no son semánticamente iguales.

El primero significa dejar el shared_ptr sin un objeto gestionado. El segundo está destinado a tener el puntero gestionar otro objeto. Esa es una distinción importante. Implementarlo en una sola función significaría que esencialmente tendremos una función que realice dos operaciones diferentes.

Además, cada operación puede tener diferentes restricciones en los tipos en cuestión. Si los volcamos en una función, entonces “ambas twigs” tendrán que satisfacer las mismas restricciones, y eso es innecesariamente restrictivo. C ++ 17 y constexpr if mitigan, pero esas funciones se especificaron antes de que se cerrara.

En última instancia, creo que este diseño está en línea con el consejo de Scott Meyers. Si el argumento predeterminado te hace hacer algo semánticamente diferente, probablemente debería ser otra sobrecarga.


Está bien, para abordar su edición. Sí, las especificaciones de excepción son diferentes. Pero como mencioné anteriormente, la razón por la que pueden ser diferentes es que las funciones están haciendo cosas diferentes. La semántica de los miembros de reset requiere esto:

 void reset() noexcept; 

Efectos : Equivalente a shared_ptr().swap(*this) .

 template void reset(Y* p); 

Efectos : Equivalente a shared_ptr(p).swap(*this) .

No hay una gran noticia de última hora allí. Cada función tiene el efecto de construir un nuevo shared_ptr con el argumento dado (o falta del mismo), y el intercambio. Entonces, ¿qué hacen los constructores shared_ptr ? Según una sección anterior , hacen esto:

 constexpr shared_ptr() noexcept; 

Efectos : construye un objeto shared_ptr vacío.
Condiciones posteriores : use_count() == 0 && get() == nullptr .

 template explicit shared_ptr(Y* p); 

Postcondiciones : use_count() == 1 && get() == p . Emite : bad_alloc , o una excepción definida por la implementación cuando no se pudo obtener un recurso que no sea la memoria

Tenga en cuenta las diferentes condiciones posteriores en el conteo de uso del puntero. Eso significa que la segunda sobrecarga debe dar cuenta de cualquier contabilidad interna. Y muy probablemente asigne almacenamiento para ello. Los dos constructores sobrecargados hacen cosas diferentes, y como dije anteriormente, es un fuerte indicio para separarlos en diferentes funciones. El hecho de que se pueda obtener una garantía de excepción más fuerte es una prueba más de la solidez de esa elección de diseño.

Y finalmente, ¿por qué unique_ptr tiene una sola sobrecarga para ambas acciones? Porque el valor predeterminado no cambia la semántica. Solo tiene que hacer un seguimiento del nuevo valor del puntero. El hecho de que el valor sea nulo (ya sea por el argumento predeterminado o de otra manera), no cambia el comportamiento de la función drásticamente. Una sola sobrecarga es por lo tanto sana.

Si a menudo está restableciendo a nullptr precisamente en lugar de un nuevo valor, la función separada void shared_ptr::reset() noexcept; tendrá una ventaja de espacio, ya que puede usar una función para todos los tipos Y , en lugar de tener una función específica que tome un tipo Y para cada tipo de Y Otra ventaja de espacio es que la implementación sin un argumento no necesita un argumento pasado a la función.

Por supuesto, tampoco importa mucho si la función se llama muchas veces.

También hay una diferencia en el comportamiento de excepción, que puede ser muy importante, y creo que esta es la motivación de por qué el estándar tiene múltiples declaraciones de esta función.

Si bien las opciones de diseño de las otras respuestas son todas válidas, asumen una cosa que no se aplica completamente aquí: ¡Equivalencia semántica!

 void shared_ptr::reset() noexcept; // ^^^^^^^^ template  void shared_ptr::reset(Y* ptr); 

La primera sobrecarga es noexcept , mientras que la segunda sobrecarga no lo es. No hay manera de decidir la noexcept función del valor de tiempo de ejecución del argumento, por lo que se necesitan las diferentes sobrecargas.

Parte de la información de fondo sobre el motivo de las diferentes especificaciones de noexcept : reset() no se lanza, ya que se supone que el destructor del objeto previamente contenido no se lanza. Pero la segunda sobrecarga podría necesitar además asignar un nuevo bloque de control para el estado del puntero compartido, que arrojará std::bad_alloc si la asignación falla. (Y el reset ting a un nullptr se puede hacer sin asignar un bloque de control).

Hay una diferencia fundamental entre una sobrecarga y un puntero predeterminado:

  • la sobrecarga es autónoma: el código de la biblioteca es completamente independiente del contexto de llamada.
  • el parámetro predeterminado no es autónomo, sino que depende de la statement utilizada en el contexto de la llamada. Se puede redefinir en un ámbito dado con una statement simple (por ejemplo, un valor predeterminado diferente o ya no hay un valor predeterminado).

Hablando semánticamente, el valor predeterminado es un atajo incrustado en el código de llamada, mientras que la sobrecarga es un significado incrustado en el código llamado.

    Intereting Posts