Дополнительные параметры функции: использовать аргументы по умолчанию (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
просто взломать?). И разница в производительности, вызванная вызовом косвенных функций, вероятно, оптимизирована.
Тем не менее, я часто вижу первое решение в коде. Есть ли веские причины, чтобы предпочесть его, кроме лжи программиста?
Ответы
Ответ 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 и обрабатывать его соответствующим образом.