Обсуждалось наверное миллионы раз, но в очередной раз имел спор с коллегой на эту тему, посему решил сформулировать свой подход.
Проблема: использовать ли не константные ссылки в аргументах функций?
Мой подход: нет.
Почему? Причина тут только одна: использование неконстантной ссылки для аргумента функции скрывает в вызывающем коде факт возможной модификации объекта, передаваемого в качестве параметра.
Лично нарвался недавно на собственную мину (конечно, упрощенный вариант):
T a = 10;
...
f(a);
...
assert(a == 10); // BOOM!!! WTF?! Who changed this?
А причина в «void f(T& a);», а должно быть «void f(const T& a);» или «void f(T* a);». Функция «f()» почему-то изменила значение «а», а писал я ее давно и успел забыть такую ее «особенность». Но из кода «f(a)» сходу не видно – может эта функция изменить «а» или нет.
А как могло произойти, что мне вообще пришло в голову сделать параметр «a» неконстантной ссылкой? Лично у меня это случается, когда переменная изначально была внутри функции, и в какой-то момент я решил сделать ее параметром, а менять в коде везде «a.» на «a->» было просто лень, вот и сделал ссылку, вместо указателя. За что и поплатился, позже.
Кстати, один из аргументов, приводимый людьми, выступающими за неконстантные ссылки –это «писать "a." приятнее и понятнее, чем "a->" или "*a"». Также ссылка более надежна с точки зрения NULL (сделать ссылку, указывающую на NULL конечно можно, но тут уже надо постараться). Тут можно выйти положения так:
void f(T* ptr_a) {
assert(ptr_a != NULL);
T& a = *ptr_a;
...
if (a.foo()) ...
}
Небольшой лишний код, но решены обе проблемы: проверка на NULL и необходимость разыменовывать указатель каждый раз. А главное, в вызывающем коде придется писать так: “f(&a)”, что явно укажет на факт возможной модификации аргумента внутри функции.
Например, в C# есть специальной ключевое слово «ref», которое надо ставить перед аргументами в вызывающем коде, если хочется передать что-то по ссылке. По мне,это очень хорошее свойство языка.
Исключения из правила
Я пока выработал для себя одно исключение: сложные, сервисные прокси-объекты, типа потока или базы данных можно передавать по неконстантной ссылке. Но тут надо четко понимать, не вы меняете этот объект внутри функции, а он меняется сам при обращении к нему.
Например:
void print(std::ostream& os, const T& a) {
os << a;
}
или
void save_to_db(Database& db, ...) {
db.connect();
db.save(...)
...
}
А когда вы используете неконстантные ссылки?
Ну и чтобы два раза не вставать, пара личных маленьких радостей:
- Мое интервью проекту "OpenQuality".
- Уверенно решил две задачи в недавней SRM 487. А достижение в том, что во второй задаче даже применил DP, хоть и тривиальное. В процессе контеста был последним в комнате, так как долго возился с первой задачей, но потом почти все упали на фазе challenge'а и на тестах, и я оказался вторым в комнате. Кстати, настоятельно рекоммендую сайт CodeForces. Наши ребята сделали отличный сайт для контестов и регулярно их там проводят. В отличие от ТопКодера русский язык там в почете, задания предоставляются на обоих языках, и выбор язык программирования гораздо шире. Также можно там спросить совета по задачам и получить квалифицированный ответ от бойцов.
Александр, а почему не участвуете в марафонах? Там очень интересные задачи. И они больше, на мой взгляд, приближены к реалиям, чем в algorithm contest.
ОтветитьУдалить@Evgeny Baskakov: пока просто не пробовал.
ОтветитьУдалитьА я предпочитаю не заниматься работой компилятора:
ОтветитьУдалитьconst T a = 10;
...
f(a); // Ошибка компиляции
Я стараюсь использовать только константные ссылки. Подсознательно для себя уже завел правило, схожее с гугловыми гайдами по разработке на С++ - если ф-ция не изменяет входящую переменную - передаем как константную ссылку, если меняет - как указатель. К тому же надо помнить про правило, что жизнь временного объекта может продлиться на время жизни константной ссылки на него, а бывает очень удобно при вызове ф-ции написать что-то вроде f( T(10) );
ОтветитьУдалить