*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

четверг, 1 июля 2010 г.

Неконстантные ссылки в аргументах функций

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

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

void f(T& t) {
  // change ‘t’
}
...
T a;
f(a);

я предпочту передачу по указателю:

void f(T* t) {
  // change ‘*t’
}
...
T a;
f(&a);

Мой основной мотив – наглядность в вызывающем коде. Когда я вижу «&» перед аргументом, я точно знаю, что это возвращаемый параметр, и он может измениться после вызова. И мне не надо постоянно помнить, какие именно агрументы у этой функции ссылки, а какие нет.

Конечно тут есть и минусы: корявость текста в вызываемом коде, так как надо таскать за собой «*» или «->». Также неплохо бы проверять указатель на ноль.

А у вас есть предпочтения в этом вопросе?

29 комментариев:

  1. Из-за необходимости добавлять assert для указателей в каждую функцию предпочитаю ссылки.

    // Пример:
    void f(T const & input, T & output);

    Если аргумент необязательный - тогда через указатель.

    P.S. Отличный блог, с интересом читаю )

    ОтветитьУдалить
  2. Как и someone.
    Кстати, вакансии блумберга еще актуальны?

    ОтветитьУдалить
  3. Верно подмечено... Я тоже стараюсь придерживаться этого правила, а еще лучше не использовать изменяемые параметры. :) Но никогда не задумывался о том что вся соль в том, что параметр-указатель заставляет программиста сознательно указывать это при вызове!

    В Google C++ Style Guide написано то же самое. Кроме морали. :)

    И конечно ссылки всегда константные!

    ОтветитьУдалить
  4. когда имя функции становится более осознанным, становится понятно что она делает и что возвращает...
    допустим Read(stream, a) Write( stream, a )...

    ОтветитьУдалить
  5. 2Pushkoff: И какой же из этих параметров (stream, a) в каком случае модифицируется?

    По моему они одинаковые. :)

    ОтветитьУдалить
  6. @Nick: Да, но все меняется каждый день, так что тянуть не стоит. По тем ссылкам всегда актуальная информация.

    ОтветитьУдалить
  7. @Pushkoff: Да, потоки - это пожалуй единственное исключение из правил для меня.

    ОтветитьУдалить
  8. С моей тоски зрения, использование ссылки для предечи агрумента имеет один плюс - изменяемый параметр будет обязательным т.е. у пользователя ф-ции не будет возможности передать несуществующий объект(не использую хаков). Это особенно важно когда ф-ция не имеет смысла без аргументов.

    ОтветитьУдалить
  9. 2Kirill: Константный! :)

    Тут речь идет о вводе параметров в функцию. Константные ссылки рулят почти всегда. (Параметрами по умолчанию ИМХО тоже не стоит злоупотреблять)

    А вывод лучше стараться все же делать через возвращаемое значение - это вполне однозначно.

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

    И конечно всегда бывают исключения. В исключительных ситуациях можно и ассерт поставить. :)

    ОтветитьУдалить
  10. Этот комментарий был удален автором.

    ОтветитьУдалить
  11. >Google C++ Style Guide
    был бы нормальным документом, если не бы не совершенно идиотский запрет на исключения. Вот как, скажите на милость, юзать RAII и сообщать об ошибках из конструкторов?

    ОтветитьУдалить
  12. @Tier: Как я понимаю, есть определенный подход, когда конструктор делает только примитивные инициализации. Сложные логические инициализации делаются явным вызовом функций типа init, геттеров и сеттеров. Это позволяет при unit тестировании более удобно работать с классом. А если в конструкторе таки возникают исключения, то он их сам ловит и устанавливает флаг типа bad, который можно проверить извне. Противоречивая практика, но вполне жизненная. Весь Хром так написан. Кстати, его исходники - это кладезь всяких штучек и приемов.

    ОтветитьУдалить
  13. Кстати, юзать RAII можно и не имея исключений.

    ОтветитьУдалить
  14. >Как я понимаю, есть определенный подход, когда конструктор делает только примитивные инициализации. Сложные логические инициализации делаются явным вызовом функций типа init, геттеров и сеттеров.

    Очень мило... Т.е. в каждом методе я ещё и проверять должен, удосужился ли пользователь моего класса вызвать init? Правильный подход - объект либо создан, либо нет. Всё. Нельзя "наполовину родиться".

    >Это позволяет при unit тестировании более удобно работать с классом.

    Уууууу... Обещали облегчение жизни при юнит тестировании, а тут ещё и менять стиль написания программ надо? Либо юнит тестирование подстраивается под RAII, либо идёт лесом, полем...

    >А если в конструкторе таки возникают исключения, то он их сам ловит и устанавливает флаг типа bad, который можно проверить извне.

    Ещё милее... Т.е. это пользователь будет проверять объект на валидность? А если забыл, и объект наполовину создан, то можем получать странное поведение, в зависимости от того, как проинициализировались мемберы. Веселуха :D Особенно это будет заметно при подобной забывчивости в юнит тесте - один раз тест прошёл, а второй раз - нет.

    >Весь Хром так написан.

    Ну, могу выразить свои соболезнования писавшим Хром :)

    >Кстати, юзать RAII можно и не имея исключений.

    Можно код?

    ОтветитьУдалить
  15. Кстати, предложенное вами ничем не отличается от такого набора
    f(MyMegaStruct*, etc...)
    g(MyMegaStruct*, etc...)
    h(MyMegaStruct*, etc...)
    На кой вам сдали классы с init'ами - я не понимаю...

    ОтветитьУдалить
  16. @Tier
    А по-моему наличие исключений - идиотская фича C++.
    Сколько людей - столько мнений.

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

    ОтветитьУдалить
  17. >А по-моему наличие исключений - идиотская фича C++.
    И вам я тоже соболезную :)

    >Да в Google Style очень хорошо написано, почему они не рекомендуют ими пользоваться.
    Как я понял, основные грабли, которые им не нравятся, отнюдь не читаемость, а необходимость работы с либами, которые эти исключения не используют.

    ОтветитьУдалить
  18. @Tier
    > И вам я тоже соболезную :)
    А я вам :-)
    Очень сложно поддерживать код с исключениями в рабочем состоянии и передавать его другому разработчику для поддержки, просто потому что - действие в одном месте, а обработка возникшей ошибки может лежать где угодно. И фактически нет способов найти место обработки, кроме как его сесть и трассировать по шагам.

    ОтветитьУдалить
  19. 2MaMoHT:
    >просто потому что - действие в одном месте, а обработка возникшей ошибки может лежать где угодно

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

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

    Функция printf возвращает код ошибки... кто нибудь его анализирует? А если бы она бросала бы исключения (предположим что она плюсовая) - проигнорировать это было бы невозможно.

    Исключения хороши тем, что их нельзя просто проигнорировать.

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

    ОтветитьУдалить
  20. @Tier: По поводу RAII, например, это. Это тривиальный wrapper для мютексов. Там нет никаких исключений, а RAII используется.

    Как-то сложно обвинять в людей, написавших за столь короткое время лучший в мире опенсорсный браузер, что они как-то недопонимают, как писать большие и сложные проекты на С++. У меня нет причин делать это. Я весьма внимательно изучал код Хрома и мне совершенно не на что пожаловаться. Все б так писали.

    А по поводу изменения стиля для использования тестирования -- ответ "да". Ни одна утилита не протестирует код, если программист написал его абсолютно нетестируемым. А внесение в конструкторы сложной логики, особенно привязка к внешним объектам, делает такой класс не тестируемым. Если получается, что чтобы код стал тестируемым надо менять стиль -- может стиль был неверный? и его стоит поменять.

    ОтветитьУдалить
  21. >действие в одном месте, а обработка возникшей ошибки может лежать где угодно
    И? А при кодах ошибок не так, что ли? Вот есть у вас либа, которой на вход даётся путь до файла. И вот не получилась у вас её открыть. И что? Вы будете обрабатывать ошибку прямо здесь? А как вы сообщите об этом пользователю? На консоль? Или на GUI? А как вы у него попросите правильный путь?
    Нет ведь! Вы передадите код ошибки выше. И "там" уже решат как жить дальше. Это, вообще-то, правильная стратегия.

    >По поводу RAII, например, это.
    У вас там
    AutoLock(Mutex& lock) : __lock(lock) {
    __lock.Lock();
    }
    А в Mutex'е
    void Mutex::Lock() { pthread_mutex_lock(&__mutex); }
    Т.е. никакого кода ошибки не возвращается. Простите, а он всегда в посиксе лочится? В смысле, ЕМНИП, может быть и ошибка. А меж тем ни Mutex::Lock(), ни конструктор AutoLock об этом никак не сообщают.

    >Как-то сложно обвинять в людей, написавших за столь короткое время лучший в мире опенсорсный браузер, что они как-то недопонимают, как писать большие и сложные проекты на С++.

    Ну, во-первых, давайте ещё холивар про "лучший браузер" здесь разводить не будем ;)
    Во-вторых, не надо давить авторитетом. Типа "они крутые пацаны, я буду делать как они". Какой-никакой, а я инженер. И меня интересуют доводы, а не авторитеты. Пока что мне так и не объяснили, чем набор функций по работе со структурой хуже класса, где инициализация вызывается отдельно от конструктора.
    В-третьих, я не говорил, что программисты гугла не умеют делать большие проекты. Их аргументацию про либы без исключений я понял. А вот 1) как использовать RAII без исключений и 2) почему _все_ проекты на плюсах не должны их юзать - нет.

    ОтветитьУдалить
  22. По поводу unit тестов - я не против, что интерфейс класса должен быть таким, чтобы его можно было разумным образом протестировать. Но, простите, отказываться от инициализации в конструкторе и исключений - это перебор.

    ОтветитьУдалить
  23. 2Tier:
    >>действие в одном месте, а обработка возникшей ошибки может лежать где угодно
    >И? А при кодах ошибок не так, что ли?

    Вот и я про то же... ошибка обрабатывается совершенно не там где возникла, а там где можно с этим что-то сделать.

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

    В то время как исключения совершенно снимают с нас эту головную боль, кроме того дают нам возможность максимально обогатить информацию об ошибке.

    Исключения однозначно рулят. Но конечно, если код is not exception-tolerant, как говорят в Google, то использование исключений может вызвать различные проблемы. Но разве это проблема исключений?

    ОтветитьУдалить
  24. +1 предыдущему оратору :)

    ОтветитьУдалить
  25. По хорошему, вызов pthread_mutex_lock надо обернуть в assert, так как если не работает pthread_mutex_lock, то программе нечего дальше делать вообще.

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

    Поэтому лично я стараюсь делать так, что конструктор может "упасть" или даже бросить исключение только из-за ошибок распределения памяти. Как раз для того, чтобы не быть рожденым наполовину. Все касаемо бизнес-логики - это уже не ответственность конструктора. Физически создав объект, он свою часть работы выполнил. Далее уже сеттеры и геттеры или функции логической инициализации.

    ОтветитьУдалить
  26. >По хорошему, вызов pthread_mutex_lock надо обернуть в assert, так как если не работает pthread_mutex_lock, то программе нечего дальше делать вообще.

    Неужели? Даже если это в плагине? Я должен всё положить ради него?
    И вообще, assert, невозбранно роняющий систему - это то ещё зло, которое даже ошибку в лог положить не позволяет. Или логи у Ъ парней из гугла тоже не в ходу? :)

    >Все касаемо бизнес-логики - это уже не ответственность конструктора. Физически создав объект, он свою часть работы выполнил.

    Я всё же считаю, что _физически_ объект создаёт аллокатор. Ибо _физически_ он не более, чем кусок памяти. А вот всё остальное - уже логика. И именно поэтому "получение ресурса есть инициализация".

    Остальной текст на мой взгляд является здравой логикой. же говорю о том, что гугловский гайд как лекарство и панацея, мягко говоря, не выдерживает критики. Не годен он для всех проектов на плюсах. Из-за тех же исключений. Из-за неиспользования RTTI. Вот мне, например, надо было сделать множественную диспетчеризацию. Как её сделать без RTTI? Паттерн Visitor не предлагать, ибо требуется пересборка всего при добавлении нового класса, а меня это не устраивает...

    ОтветитьУдалить
  27. Не ожидал, что такой безобидный пост вызовет такую горячую дискусию.

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

    ОтветитьУдалить
  28. Я избегаю исключений. Error codes + init() FTW.
    О недостатках исключений здесь: http://yosefk.com/c++fqa/exceptions.html
    Вообще весь FQA - полезное чтиво.

    ОтветитьУдалить
  29. Прислали в комментариях интересную ссылку (непонятно, почему не отображается сам комментарий): http://yosefk.com/c++fqa/exceptions.html. Например, в п.17.2 есть неплохие аргументы против бросания сложных исключений в конструкторах.

    ОтветитьУдалить