Подтвердить что ты не робот

Дополнительные параметры функции: использовать аргументы по умолчанию (NULL) или перегрузить функцию?

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

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

Сделайте это указателем и сделайте его 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
}

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

В этом контексте целью 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(), чтобы взять ссылку вместо указателя, то и интуитивно понятно, что у вызывающего уже должен быть вектор, и им будет сложно переносить то, что не является готовым к движению вектором.

Ответ 3

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

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

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

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

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

Ответ 4

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

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

Ответ 5

В С++ ссылки не могут быть NULL, очень хорошим решением было бы использовать шаблон 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()) {

   }
}

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

SomeFunction(NULL, pName, NULL, pDestination);

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

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);

Ответ 9

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

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

Чтобы поместить некоторые примеры кода на каждый:

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

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);
}

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

Ответ 10

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

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

У меня была бы одна функция, которая принимает параметр, и тот, который называется "createVectorAndFoo" или что-то в этом роде (очевидно, что именование становится проще с реальными проблемами).

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

Ответ 11

В общем, я согласен с предложением других использовать двухфункциональный подход. Однако, если вектор, созданный при использовании 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) {
    ...
}

Ответ 12

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

Со вторым вы можете проверить (утверждать, что угодно) для NULL и обрабатывать его соответствующим образом.