Необязательные параметры функции: использовать аргументы по умолчанию (NULL) или перегрузить функцию? - PullRequest
40 голосов
/ 01 апреля 2009

У меня есть функция, которая обрабатывает данный вектор, но может также создать сам такой вектор, если он не задан.

Я вижу два варианта дизайна для такого случая, когда параметр функции является необязательным:

Сделать указатель и NULL по умолчанию:

void foo(int i, std::vector<int>* optional = NULL) {
  if(optional == NULL){
    optional = new std::vector<int>();
    // fill vector with data
  }
  // process vector
}

Или иметь две функции с перегруженным именем, одна из которых пропускает аргумент:

void foo(int i) {
   std::vector<int> vec;
   // fill vec with data
   foo(i, vec);
}

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

Есть ли причины предпочитать одно решение другому?

Я немного предпочитаю второй, потому что я могу сделать вектор ссылкой const, так как он, когда предоставляется, только для чтения, а не для записи. Кроме того, интерфейс выглядит чище (не NULL просто взломать?). И разница в производительности, вызванная косвенным вызовом функции, вероятно, оптимизируется.

Тем не менее, я часто вижу первое решение в коде. Есть ли веские причины, чтобы предпочесть это, кроме лени программиста?

Ответы [ 12 ]

41 голосов
/ 01 апреля 2009

Я бы не использовал ни один из подходов.

В этом контексте, похоже, цель foo () - обрабатывать вектор. То есть работа foo () заключается в обработке вектора.

Но во второй версии foo () неявно дано второе задание: создать вектор. Семантика между foo () версии 1 и foo () версии 2 не одинакова.

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

Например:

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

std::vector<int>* makeVector() {
   return new std::vector<int>;
}

Очевидно, что эти функции тривиальны, и если все, что нужно сделать makeVector () для выполнения своей работы, это просто вызов new, то, возможно, нет смысла иметь функцию makeVector (). Но я уверен, что в вашей реальной ситуации эти функции делают намного больше, чем показано здесь, и мой код выше иллюстрирует фундаментальный подход к семантическому дизайну: дает одной функции одну работу для выполнения .

Схема, которую я описал выше для функции foo (), также иллюстрирует еще один фундаментальный подход, который я лично использую в своем коде, когда речь идет о разработке интерфейсов, который включает сигнатуры функций, классы и т. Д. Вот что: я считаю, что хороший интерфейс: 1) прост и интуитивно понятен в использовании правильно, и 2) трудно или невозможно использовать неправильно . В случае функции foo () мы косвенно говорим, что при моем дизайне вектор должен существовать и быть «готовым». Сконструировав функцию foo (), чтобы она брала ссылку вместо указателя, интуитивно понятно, что вызывающая сторона должна уже иметь вектор, и им будет трудно передать что-то, что не является готовым вектором .

28 голосов
/ 01 апреля 2009

Я бы определенно предпочел 2-й подход перегруженных методов.

Первый подход ( необязательные параметры ) стирает определение метода, поскольку он больше не имеет единственной четко определенной цели. Это, в свою очередь, увеличивает сложность кода, затрудняя его понимание тем, кто не знаком с ним.

При втором подходе ( перегруженные методы ) каждый метод имеет четкую цель. Каждый метод хорошо структурирован и связан . Некоторые дополнительные примечания:

  • Если есть код, который необходимо дублировать в оба метода, это может быть выделено в отдельный метод , и каждый перегруженный метод может вызвать этот внешний метод.
  • Я бы пошел еще дальше и назвал каждый метод по-разному , чтобы указать на различия между методами. Это сделает код более самодокументированным.
21 голосов
/ 21 февраля 2012

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

Значения параметров по умолчанию:
Во-первых, я хочу отметить, что при первоначальном проектировании проекта должно быть мало или не нужно использовать по умолчанию, если он хорошо спроектирован. Тем не менее, где основные активы по умолчанию вступают в игру с существующими проектами и хорошо зарекомендовавшими себя API. Я работаю над проектами, которые состоят из миллионов существующих строк кода и не могут позволить себе перекодировать их все. Поэтому, когда вы хотите добавить новую функцию, которая требует дополнительного параметра; для нового параметра требуется значение по умолчанию. В противном случае вы сломаете всех, кто использует ваш проект. Это было бы хорошо для меня лично, но я сомневаюсь, что ваша компания или пользователи вашего продукта / API будут благодарны за перекодировку своих проектов при каждом обновлении. Проще говоря, настройки по умолчанию отлично подходят для обратной совместимости! Это обычно причина, по которой вы увидите настройки по умолчанию в больших API или существующих проектах.

Переопределения функций: Преимущество переопределений функций заключается в том, что они позволяют совместно использовать концепцию функциональности, но с другими опциями / параметрами. Тем не менее, часто я вижу, что переопределения функций лениво используются для обеспечения совершенно другой функциональности, только с немного другими параметрами. В этом случае каждая из них должна иметь отдельно названные функции, относящиеся к их конкретной функциональности (как в примере с OP).

Эти возможности c / c ++ хороши и работают при правильном использовании. Что можно сказать о большинстве любых функций программирования. Именно когда они подвергаются насилию / злоупотреблению, они вызывают проблемы.

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

6 голосов
/ 28 ноября 2012

Ссылки не могут быть NULL в C ++, действительно хорошим решением будет использование шаблона Nullable. Это позволит вам делать вещи, это ref.isNull ()

Здесь вы можете использовать это:

template<class T>
class Nullable {
public:
    Nullable() {
        m_set = false;
    }
    explicit
    Nullable(T value) {
        m_value = value;
        m_set = true;
    }
    Nullable(const Nullable &src) {
        m_set = src.m_set;
        if(m_set)
            m_value = src.m_value;
    }
    Nullable & operator =(const Nullable &RHS) {
        m_set = RHS.m_set;
        if(m_set)
            m_value = RHS.m_value;
        return *this;
    }
    bool operator ==(const Nullable &RHS) const {
        if(!m_set && !RHS.m_set)
            return true;
        if(m_set != RHS.m_set)
            return false;
        return m_value == RHS.m_value;
    }
    bool operator !=(const Nullable &RHS) const {
        return !operator==(RHS);
    }

    bool GetSet() const {
        return m_set;
    }

    const T &GetValue() const {
        return m_value;
    }

    T GetValueDefault(const T &defaultValue) const {
        if(m_set)
            return m_value;
        return defaultValue;
    }
    void SetValue(const T &value) {
        m_value = value;
        m_set = true;
    }
    void Clear()
    {
        m_set = false;
    }

private:
    T m_value;
    bool m_set;
};

Теперь вы можете иметь

void foo(int i, Nullable<AnyClass> &optional = Nullable<AnyClass>()) {
   //you can do 
   if(optional.isNull()) {

   }
}
5 голосов
/ 01 апреля 2009

Согласен, я бы использовал две функции. По сути, у вас есть два разных варианта использования, поэтому имеет смысл иметь две разные реализации.

Я считаю, что чем больше кода на C ++ я пишу, тем меньше у меня значений параметров по умолчанию - я бы не стал терять слезы, если эта функция устарела, хотя мне пришлось бы переписать потерянную загрузку старого кода!

3 голосов
/ 01 апреля 2009

Я обычно избегаю первого случая. Обратите внимание, что эти две функции отличаются в том, что они делают. Один из них заполняет вектор некоторыми данными. Другой не (просто принять данные от абонента). Я по-разному называю функции, которые на самом деле делают разные вещи. На самом деле, даже когда вы пишете их, они представляют собой две функции:

  • foo_default (или просто foo)
  • foo_with_values

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

2 голосов
/ 02 июля 2009

Я прямо в лагере "перегрузки". Другие добавили подробности о вашем действительном примере кода, но я хотел бы добавить, как мне кажется, преимущества использования перегрузок по сравнению со значениями по умолчанию для общего случая.

  • Любой параметр может быть «по умолчанию»
  • Нет ошибки, если переопределяющая функция использует другое значение по умолчанию.
  • Нет необходимости добавлять «хакерские» конструкторы к существующим типам, чтобы они могли иметь значения по умолчанию.
  • Выходные параметры могут быть установлены по умолчанию без необходимости использования указателей или хакерских глобальных объектов.

Чтобы добавить несколько примеров кода для каждого:

Любой параметр может быть установлен по умолчанию:

class A {}; class B {}; class C {};

void foo (A const &, B const &, C const &);

inline void foo (A const & a, C const & c)
{
  foo (a, B (), c);    // 'B' defaulted
}

Нет опасности переопределения функций, имеющих разные значения по умолчанию:

class A {
public:
  virtual void foo (int i = 0);
};

class B : public A {
public:
  virtual void foo (int i = 100);
};


void bar (A & a)
{
  a.foo ();           // Always uses '0', no matter of dynamic type of 'a'
}

Нет необходимости добавлять «хакерские» конструкторы к существующим типам, чтобы их можно было использовать по умолчанию:

struct POD {
  int i;
  int j;
};

void foo (POD p);     // Adding default (other than {0, 0})
                      // would require constructor to be added
inline void foo ()
{
  POD p = { 1, 2 };
  foo (p);
}

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

void foo (int i, int & j);  // Default requires global "dummy" 
                            // or 'j' should be pointer.
inline void foo (int i)
{
  int j;
  foo (i, j);
}

Единственное исключение из правила перегрузки по сравнению со значениями по умолчанию - для конструкторов, где в данный момент конструктор не может пересылать другому. (Я верю, что C ++ 0x это решит, хотя).

2 голосов
/ 01 апреля 2009

В C ++ вы должны по возможности избегать допустимых параметров NULL. Причина в том, что это существенно сокращает документацию по сайту. Я знаю, что это звучит экстремально, но я работаю с API, которые принимают более 10-20 параметров, половина из которых может быть действительно NULL. Полученный код практически не читается

SomeFunction(NULL, pName, NULL, pDestination);

Если вы переключите его для принудительной ссылки на константу, код просто будет более читабельным.

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);
2 голосов
/ 01 апреля 2009

Я тоже предпочитаю второй. Хотя между ними нет большой разницы, вы в основном используете функциональность основного метода в перегрузке foo(int i), и первичная перегрузка будет работать идеально, не заботясь о существовании отсутствия другого, так что в версии с перегрузкой больше разделений.

1 голос
/ 01 апреля 2009

Как правило, я согласен с предложением других использовать двухфункциональный подход. Однако, если вектор, созданный при использовании формы с 1 параметром, всегда один и тот же, вы можете упростить задачу, вместо этого сделав его статичным и используя вместо него параметр const& по умолчанию:

// Either at global scope, or (better) inside a class
static vector<int> default_vector = populate_default_vector();

void foo(int i, std::vector<int> const& optional = default_vector) {
    ...
}
...