Аргумент по умолчанию против перегрузок в С++

Например, вместо

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

можно подумать

template <typename Y = T>
void shared_ptr::reset(Y* ptr = nullptr);

Я думаю, что разница в производительности здесь незначительна, а вторая версия более кратка. Есть ли какая-то конкретная причина, по которой стандарт C++ идет первым путем?

Тот же вопрос задан для языка Kotlin, и аргумент по умолчанию предпочтительнее.

Обновить:

std::unique_ptr::reset() следует за конструкцией аргументов по умолчанию (см. здесь). Поэтому я думаю, что причина, по которой std::shared_ptr::reset() использует перегрузки, состоит в том, что они имеют разные спецификации исключений.

Ответ 1

Важнейшим отличием является то, что две операции на самом деле не семантически одинаковы.

Первый означает, что оставить shared_ptr без управляемого объекта. Второй означает, что указатель управляет другим объектом. Это важное различие. Реализация его в одной функции означала бы, что у нас по существу будет одна функция, выполняющая две разные операции.

Кроме того, каждая операция может иметь различные ограничения на типы, о которых идет речь. Если мы сбрасываем их в одну функцию, то "обе ветки" должны будут удовлетворять тем же ограничениям и это бесполезно ограничительное. С++ 17 и constexpr if смягчить, но эти функции были указаны до выхода.

В конечном счете, я думаю, что этот дизайн соответствует совету Скотта Майерса. Если аргумент по умолчанию вы делаете что-то семантически другое, возможно, это будет другая перегрузка.


Хорошо, так, чтобы обратиться к вашему редактированию. Да, спецификации исключений разные. Но, как я уже упоминал ранее, причина, по которой они могут быть разными, заключается в том, что функции выполняют разные вещи. Семантика членов reset требует:

void reset() noexcept;

Эффекты: эквивалент shared_ptr().swap(*this).

template<class Y> void reset(Y* p);

Эффекты: эквивалент shared_ptr(p).swap(*this).

Там не большой новостной флеш. Каждая функция имеет эффект построения нового shared_ptr с заданным аргументом (или его отсутствием) и свопинга. Итак, что делают конструкторы shared_ptr? Согласно предыдущему разделу, они делают следующее:

constexpr shared_ptr() noexcept;

Эффекты: Создает пустой объект shared_ptr.
Постусловия: use_count() == 0 && get() == nullptr.

template<class Y> explicit shared_ptr(Y* p);

Постусловия: use_count() == 1 && get() == p. Броски: bad_alloc или исключение, определяемое реализацией, когда невозможно получить ресурс, отличный от памяти

Обратите внимание на разные условия сообщения для подсчета использования указателя. Это означает, что вторая перегрузка должна учитывать любую внутреннюю бухгалтерскую отчетность. И, скорее всего, выделить для него хранилище. Два перегруженных конструктора делают разные вещи, и, как я уже говорил, это сильный намек, чтобы разделить их на разные функции. Тот факт, что можно получить более сильную гарантию исключения, еще раз подтверждает надежность этого дизайна.

И, наконец, почему у unique_ptr есть только одна перегрузка для обоих действий? Поскольку значение по умолчанию не изменяет семантику. Он просто должен отслеживать новое значение указателя. Тот факт, что значение равно null (либо по аргументу по умолчанию, либо по-другому), не меняет поведение функции. Поэтому одна перегрузка является звуковой.

Ответ 2

Если вы OFTEN переустанавливаете значение точно nullptr а не новое значение, тогда отдельная функция void shared_ptr::reset() noexcept; будет иметь пространственное преимущество, поскольку вы можете использовать одну функцию для всех типов Y, а не иметь определенную функцию, которая принимает тип Y для каждого типа Y Еще одним преимуществом пространства является то, что реализация без аргумента не нуждается в аргументе, переданном функции.

Конечно, не имеет большого значения, если функция называется много раз.

Существует также различие в поведении исключений, которое может быть очень важным, и я считаю, что это мотивация относительно того, почему стандарт имеет несколько деклараций этой функции.

Ответ 3

В то время как выбор дизайна других ответов действительно, они принимают одно из положений, которое в полной мере не применяется здесь: семантическая эквивалентность!

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

Первая перегрузка noexcept, в то время как вторая перегрузка нет. Там нет никакого способа решить noexcept -ness на основе значения выполнения аргумента, поэтому необходимы различные перегрузки.

Некоторая справочная информация о причине для разных спецификаций noexcept: reset() не выбрасывает, поскольку предполагается, что деструктор ранее содержащегося объекта не выбрасывает. Но второй перегрузке может потребоваться дополнительно выделить новый блок управления для состояния общего указателя, который будет std::bad_alloc если распределение не выполняется. (И reset ting на nullptr можно, не выделяя блок управления.)

Ответ 4

Существует принципиальное различие между указателем перегрузки и указателем по умолчанию:

  • перегрузка является самодостаточной: код в библиотеке полностью не зависит от вызывающего контекста.
  • параметр по умолчанию не является самодостаточным, но зависит от объявления, используемого в вызывающем контексте. Он может быть переопределен в заданной области с простым объявлением (например, другим значением по умолчанию или значением по умолчанию больше.

Таким образом, семантически говоря, значение по умолчанию - это короткий фрагмент, заключенный в вызывающий код, тогда как перегрузка - это значение, встроенное в вызываемый код.