Несколько лет назад среди C++ блоггеров завирусилась первоапрельская шутка о том, что C++ задепрекейтил указатели (например, Fluent C++ — в C++ больше не будет указателей). Что ж, поскольку C++ почти никогда ничего не депрекейтит, это была полная умора. Однако действительно ли нам до сих пор так необходимы указатели? Именно этот вопрос я и хочу осветить в сегодняшней статье.

О чем говорят указатели

В C++ указатель сигнализирует о том, что параметр может не иметь значения. Всякий раз, когда функция получает указатель, мы должны реализовать в ее теле проверку, является ли параметр nullptr. К сожалению, я постоянно вижу код, в котором этой проверкой пренебрегают. Никакая документация и комментарии типа “valid non-null object is required” не избавляют нас от необходимости совершать эту проверку.

Я также видел случаи, когда проверка на nullptr в функции была опущена попросту потому что было сложно решить, что делать в случае nullptr. Например, когда nullptr получает функция, которая возвращает void.

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

Используйте ссылки вместо указателей

Как только мы переходим на ссылки, необходимость проверки и бесконечные комментарии канут в Лету. В отличие от указателя, ссылка говорит нам, что на данном этапе требуется валидный объект.

Самый простой подход — по-прежнему получать указатели на границах API, если, например, у нас нет возможности изменить API. Но затем, в теле функции первым делом выполнять проверку на nullptr и делать return, если указатель null. Если он валидный, разыменовывать указатель и сохранять в ссылке.

bool DoSomeStuff(int* data)
{
  if(nullptr == data) { return false; }

  int& refData = *data;

  return HandleData(refData);
}

Таким образом, у нас хотя бы есть возможность сделать наш код и внутреннее API чуть-чуть опрятнее. Возможно, в следующем релизе мы сможем наводить порядок и в общедоступном API.

Постойте, мне нужен опциональный параметр

Хорошо, допустим, мы изменим все указатели на ссылки. Но что, если нам нужен параметр, который можно не предоставлять (maybe parameter)? Ничего не приходит на ум? Именно, опциональность! В этом случае C++17 предоставляет нам std::optional. Поэтому, пожалуйста, перестаньте злоупотреблять указателями, когда хотите указать, что параметр является необязательным. Не нужно преобразовывать int в int* только для того, чтобы для сравнения было доступно значение nullptr.

bool DoSomeStuff(std::optional<int> data)
{
  if(data.has_value()) { return HandleData(data.value()); }

  return false;
}

Тип данных std::optional подходит для этих целей гораздо лучше, чем указатели. Такие функции, как get_value_or, избавляют нас от необходимости добавления if, который будет устанавливать либо переданное значение, либо значение по умолчанию.

Хорошо, а что на счет массивов? Допустим, мы хотим передать в функцию массив — здесь мы не можем использовать ссылки, за исключением тех случаев, когда мы делаем это через шаблон. О, и, пожалуйста, не вспоминайте о std::array, потому что я хочу, чтобы эту функцию можно было вызывать с различными размерами массивов. Похоже здесь нам уж точно не обойтись без указателей!

void IWantPointers(const char* data, const size_t length)
{
  for(int i = 0; i < length; ++i) { std::cout << data[i]; }
}

void Use()
{
  char data[]{"Hello, Pointers\n"};

  IWantPointers(data, sizeof(data));
}

span и string_view спешат на помощь

А вот и нет. По крайней мере, нам не нужны указатели в API функции. C++20 предоставляет нам std::span для случаев, когда мы хотим передать массив или непрерывный контейнер (в этом примере мы также можем использовать std::string_view из C++17). Преимущество std::span заключается в том, что он содержит информацию о количестве элементов данных. Таким образом, нам не нужен дополнительный параметр под размер массива и нужно на порядок меньше sizeof.

void IWantPointers(std::span<const char> data)
{
  for(const auto& c : data) { std::cout << c; }
}

void Use()
{
  char data[]{"Hello, Pointers\n"};

  IWantPointers(data);
}

Думаю, сейчас мы находимся на этапе, когда можно сказать, что у нас практически нет необходимости задействовать указатели в API верхнего уровня. С такими вспомогательными типами, как std::optional и std::span, наша жизнь стала намного лучше. И да, указатели по-прежнему существуют в C++, и что бы я здесь не говорил, их право на существование неоспоримо. К примеру, std::span принимает и возвращает указатель.

Почему меня это так волнует?

Ну, мне просто нравятся чистые и выразительные API. Что мне еще нравится, так это эффективный код. Взгляните на следующий пример в Compiler Explorer и убедитесь сами. Мы видим программу целиком, включая main. Функции Fun, которая принимает указатель и проверяет на nullptr, потребовалось 7 инструкций при -O3. Версии без проверки, как и версии со ссылкой, потребовалось всего 3 инструкции. И это учитывая, что компилятор видит всю программу целиком! Самое интересное — это Opt. Здесь я использую std::optional в связке с get_value_or. То есть происходит проверка значения. Однако и Clang, и GCC удается скомпилировать эту функцию в 6 строк ассемблерного кода. Неплохо, правда? Здесь отсутствует библиотечная часть, поэтому мы еще и получаем дополнительные накладные расходы за сам optional.

Так нужны ли нам указатели?

Что ж, надеюсь, я показал вам, что мы, по крайней мере, нуждаемся в них реже, чем раньше. Указатели по-прежнему являются важной частью C++, но во многих местах мы можем использовать более подходящие типы данных.


Идея модульного тестирования прочно укрепилась среди лучших практик по разработке ПО. Во многих языках это выражено в наличии специальных инструментов, которые могут поставляться в составе стандартной библиотеки. Но в языке С++ ситуация иная — имеется несколько популярных сторонних библиотек, с помощью которых пишутся модельные тесты. Одна из таких библиотек называется GoogleTest. Приглашаем на открытое заняте, на котором мы рассмотрим, как выглядит использование этой библиотеки для тестирования C++ кода. Регистрация на занятие.

Комментарии (49)


  1. Ritan
    03.06.2022 19:01
    +6

    Вот только std::optional<int&> невалиден, так что как минимум для этого указатели всё-ещё нужны


    1. me21
      04.06.2022 12:27
      +2

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


      1. mayorovp
        04.06.2022 13:12

        Ну вот как раз для тех случаев когда значения может не быть.


        1. me21
          04.06.2022 13:15
          +1

          А почему не написать std::optional<int>? Зачем нам там именно ссылка в этом случае?


          1. mayorovp
            04.06.2022 13:19
            +3

            В общем случае там нее int может быть, а разделяемый объект.


    1. Akon32
      04.06.2022 13:55

      std::optional<std::shared_ptr<int>> ?


      1. Ritan
        04.06.2022 16:14
        +4

        shared_ptr и так может выражать отсутствие значения, optional тут не нужен. К тому умные указатели это всё ещё указатели против которых эта статья и голосует


    1. gwisp
      06.06.2022 07:22

      Дополню ещё 2 места, где без указателей никак:

      1. Поля типа Foo* в классе, если требуется менять значение этого поля. Референсы менять нельзя.

      2. shared_ptr и unique_ptr - там где, в деструкторе нужно удалять другие объекты. Если использовать референсы, то придётся руками писать удаление в деструкторе.


      1. Ingulf
        06.06.2022 09:24

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


    1. Ingulf
      06.06.2022 09:16

      невалиден, как и во всем stl, впрочем тут нужен не указатель, а std::reference_wrapper<>


  1. semenyakinVS
    03.06.2022 19:57
    +3

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


  1. pehat
    03.06.2022 20:15
    +2

    Какой адовый треш. И этот человек себя позиционирует как C++ trainer.

    #include <iostream>
    #include <string>
    
    using namespace std;
    
    void foo(int* p) {
      if (p == nullptr) return;
      int& x = *p;
      cout << x << "\n"; // output: 42
      delete p; // this can happen externally, especially in a multi-threaded program
      cout << x << "\n"; // output: UB
    }
    
    int main()
    {
      int* p = new int(42);
      foo(p);
    }

    Я уже молчу о том, что этот самопровозглашенный коуч даже не затрагивает тему умных указателей, очевидно, потому что не знает, зачем их придумали (спойлер: именно из-за таких происшествий с ссылками).


    1. Sap_ru
      03.06.2022 20:33
      +9

      Как бы, нет. Ваш пример некорректен.

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

      Со второй придиркой аналогичные проблемы. Если вы получаете указатель а он где-то во внешнем коде освобождается пока функция не завершилась, то проблема не с указателями. Так у вас и значения в контейнерах могут измениться, и контейнеры освободиться. Указатели тут ни при чём


      1. pehat
        03.06.2022 22:24
        +11

        Если у вас значения в других потоках меняются, то тут не в указателях дело, а нужно многопоточную синхронизацию предусматривать

        А если у вас nullptr разыменовывается - нужно внимательнее смотреть, ну да, ну да. Посыл поста был в том, что ссылки лечат от разыменования nullptr, но это неправда и от прострела ноги другим способом не защищает, хотя дает ложное ощущение уверенности.

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

        Конечно, в этой ситуации проблема не с указателями, а с семантикой владения. Если у вас unique_ptr в функцию передали - на вас возложили ответственность за уничтожение объекта или же передачу его тому, кто уничтожит. Если у вас shared_ptr в функцию передали - у вас есть гарантия, что пока счетчик ссылок не обнулится, объект будет жить. Ни ссылка, ни тем более сырой указатель не дают никаких гарантий.


  1. NightShad0w
    03.06.2022 21:08
    +10

    С ног на голову перевернуто в статье. Указатели не для того, чтобы nullptr был. А что, как бы странно не звучало, УКАЗЫВАТЬ на куда-то. Побочным эффектом является существование указателя в никуда, который монадами MayBe или std::optional становится явно выраженным, но никуда не девается, а вот реактивные преобразования успешно его устраняют из семантики кода.
    Ссылка — это именованный псевдоним, который не может быть переприсвоен, поэтому элементарные алгоритмы с деревьями или динамическими списками не смогут работать с ссылками.
    Всякий раз когда функция получает указатель, нужно задать себе вопрос: а не Си интерфейс кросс-компиляторной библиотеки ли у меня? Потому что в С++ сырых указателей вообще быть уже не должно 10 лет как. А умные указатели, как бы странно не звучало, выражают не семантику указывания, а семантику ВЛАДЕНИЯ. И руководствуясь RAII, если в руках есть указатель, значит все у нас хорошо, объект существует и мы им владеем.
    Умные указатели или сырые указатели на примитивные типы в примитивных примерах — плохая иллюстрация мощных инструментов языка общего назначения.
    Таким образом, единственное место, где встречается сырой указатель, который надо проверять на валидность — границы Си библиотек. Получили указатель, проверили, переложили в нужные нам сущности типа умных указателей или по месту разыменовали в значение — и понеслись в бизнес-логику. Си-стиль проверки указателей на каждой строчке каждой функции — это следствие очень ограниченных ресурсов или плохого дизайна кода.


    1. DaylightIsBurning
      04.06.2022 00:47
      +2

      Есть ещё один случай, когда raw pointer - хороший вариант. По сути это случай когда нужен аналог weak_ptr для uniq_ptr. Процитирую Smart Pointer Guidelines:

      What about passing or returning a smart pointer by reference?

      Don't do this.

      In principle, passing a const std::unique_ptr<T>& to a function which does not take ownership has some advantages over passing a T*: the caller can't accidentally pass in something utterly bogus (e.g. an int converted to a T*), and the caller is forced to guarantee the lifetime of the object persists across the function call. However, this declaration also forces callers to heap-allocate the objects in question, even if they could otherwise have declared them on the stack. Passing such arguments as raw pointers decouples the ownership issue from the allocation issue, so that the function is merely expressing a preference about the former. For the sake of simplicity and consistency, we avoid asking authors to balance these tradeoffs, and simply say to always use raw pointers.

      Вот та же мысль в C++ Core Guidelines:

      F.7: For general use, take T* or T& arguments rather than smart pointers

      Reason

      Passing a smart pointer transfers or shares ownership and should only be used when ownership semantics are intended. A function that does not manipulate lifetime should take raw pointers or references instead.


      1. deema35
        04.06.2022 04:13
        +1

        При помощи weak_ptr можно узнать существует объект или нет, а у обычного указателя такой обратной связи нет.


        1. DaylightIsBurning
          04.06.2022 09:14

          В случае с unique_ptr предполагается что лайфтайм гарантирован и проверка не нужна.


      1. Revertis
        04.06.2022 15:05

        А в C++ нельзя передать в функцию ссылку на smart pointer?


        1. DaylightIsBurning
          04.06.2022 15:23

          1. Revertis
            04.06.2022 15:32

            Ну вот эта часть непонятна:

            However, this declaration also forces callers to heap-allocate the objects in question, even if they could otherwise have declared them on the stack.

            Вот есть у нас уже объект в смарт-поинтере, и мы передаём на него ссылку в какую-то функцию, чтобы не передавать владение или не инкрементить счётчик ссылок. Почему это плохо?

            Мне кажется, что автор этого предложения придумал какую-то странную ситуацию, которую я не могу придумать.


            1. IvaYan
              04.06.2022 16:17

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


              1. Revertis
                04.06.2022 18:09

                Так если объекты уже в куче, зачем запрещать такое?


                1. IvaYan
                  04.06.2022 19:08
                  +3

                  В цитате которую вы привели как раз про это. Использование смарт принтеров обязывает вас располагать объекты именно в куче. А использование обычных указателей или ссылок не обязывает.

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


            1. Ritan
              04.06.2022 16:18

              Речь о такой ситуации.

              void foo(int* p) {
                //something
              }
              
              void main() {
                int a = 42;
                foo(&a);
              }

              С умными указателями так сделать(без костылей) нельзя. Потому что, как там и сказано, умные указатели помимо владения ещё и управляют аллокацией.


              1. Revertis
                04.06.2022 18:08

                Ну так не надо писать `foo(int* p)`, пишите foo(int& p).


                1. Ritan
                  04.06.2022 18:54
                  +2

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


        1. Deosis
          06.06.2022 07:47

          Можно, но это может быть не оптимально.

          Например при передаче ссылки на умный указатель необходимо этот самый указатель сохранить в памяти и передать ссылку на этот участок памяти.

          Это мешает некоторым оптимизациям компилятора.


    1. leonid_m_kim
      06.06.2022 07:23

      А можно немного подробнее про то, как реактивные преобразования избавляют от указателей в никуда?

      Или - где про это почитать?


    1. 0o00oo00o0
      06.06.2022 16:37
      -1

      дерево тоже на умных указателях писать?


  1. it1804
    04.06.2022 01:10
    +11

    Я также видел случаи, когда проверка на nullptr в функции была опущена попросту потому что было сложно решить, что делать в случае nullptr. Например, когда nullptr получает функция, которая возвращает void.

    Это лучшее оправдание говнокода которое я видел. Нормально подогнать сигнатуру функции под код, но не код под сигнатуру, который ещё и работать будет с вероятностью 50/50 при этом.

    Пример из жизни. У товарища банкомат не принимает карту, я ему говорю, ладно, забей. Ну он он и забил. Буквально вдавил в картоприёмник. Неделю стрелял денег пока инкассация не прошла.


  1. deema35
    04.06.2022 03:55
    -4

    Я также видел случаи, когда проверка на nullptr в функции была опущена попросту потому что было сложно решить, что делать в случае nullptr. Например, когда nullptr получает функция, которая возвращает void.

    А в чем проблемам:

    std::cerr << "null pointer'' << std::endl;

    throw;

    Кроме того указатели понятны и естественны для большинства программистов, а std::optional выглядит как китайская грамота. Код же мы не только для себя пишем.


  1. Tsvetik
    04.06.2022 08:30
    +2

    В С++ есть ссылки на массивы и это очень удобно

    https://stackoverflow.com/a/10008405/5897995

    void foo(double (&bar)[10]) { }

    double arr[20]; foo(arr); // won't compile

    template<typename T, size_t N> void foo(T (&bar)[N]) { // use N here }


    1. Racheengel
      04.06.2022 13:07
      +3

      Это ж сарказм был, правда?


      1. Tsvetik
        04.06.2022 18:22

        Нет, не сарказм. Действительно очень удобно.


        1. Racheengel
          05.06.2022 17:14

          А чем std::vector не устраивает?


          1. F0iL
            05.06.2022 22:59
            +2

            Он heap-allocated. В каких-то случаях динамической памяти может вообще не быть (встраиваемые системы) или просто есть смысл избегать лишних аллокаций.


    1. Sap_ru
      05.06.2022 14:52

      А если тело функции не хедере, а в другом исходном файле?


      1. Tsvetik
        05.06.2022 15:41

        В каком из примеров?

        Шаблоны пишут в хидерах, а с первыми двумя примерами все хорошо


        1. Sap_ru
          05.06.2022 17:36

          Но не всякую функцию можно или целесообразно размещать в хедере.


          1. Tsvetik
            05.06.2022 19:11

            Мне надоело угадывать ваши мысли. Пишите контрпример, где ссылка на массив не работает или что там вы имели ввиду


  1. SpiderEkb
    04.06.2022 17:07

    Что мешает Благородному Дону не использовать указатели в своем коде, но использовать только ссылки?

    Вот я сейчас много пишу на достаточно специфическом языке RPG на платформе IBM i. Там указатели практически не используются. Параметры по умолчанию передаются по ссылке (или по значению, если установлен соотв. модификатор).

    dcl-pr MyProc;
      dcl-pi *n;
        Parm1   char(10) const;   // передача по ссылке, допустима передача литерала
        Parm2   char(5);          // передача по ссылке
        Parm3   int(10)  value;   // передача по значению
      end-pi;
      
      // код
      
      return;
    end-proc;

    Указатели существуют, но они не типизированы. Точнее, есть два типа указателя - на данные и за процедуру. И все. Типизация указателя осуществляется объявлением переменной на которую он указывает.

    dcl-s  String   char(50);               // Строка 50 символов
    dcl-s  Str20    char(20)  based(ptr1);  // Строка 20 символов, 
                                            // на которую указывает указатель ptr1
    dcl-s  Str30    char(30)  based(ptr2);  // Строка 20 символов, 
                                            // на которую указывает указатель ptr2
    dcl-s  Ptr      pointer;                // Просто указатель
    
    Ptr = %addr(String);                    // Получили адрес строки
    ptr1 = Ptr;                             // Теперь Str20 совпадает с первыми 
                                            // 20-ю символами String
    ptr2 = Ptr + 20;                        // А Str30 - String c 21-го символа

    И ничего. Вполне жизнеспособно. Если есть нужда, то можно и с указателями поработать. А можно и без них обходиться запросто.

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

    struct tagDGMHeader {
      int id            // уникальный идентификатор
      int len;          // размер блока данных
      int type;         // тип датаграммы
      int source;       // отправитель
      int destination;  // получатель
    };
    
    struct tagDGMType1 : tagDGMHeader {
      char Data[256];   // какие=то данные
    };
    
    struct tagDGMType2 : tagDGMHeader {
      int Data[16];   // какие=то данные
    };
    
    struct tagDGMType3 : tagDGMHeader {
      int  data1;   
      char data2[64];
    };

    Тип датаграммы, может быть, например 0, 1, 2,...

    Тогда делаем диспетчер датаграмм примерно так

    int DGMHndlr1(void *pdgm)
    {
      int rslt = 0;
      tagDGMType1 *pdgmtype1 = (tagDGMType1*)pdgm;
      
      // тут обработка датаграммы типа 1
      
      return rslt;
    }
    
    int DGMHndlr2(void *pdgm)
    {
      int rslt = 0;
      tagDGMType2 *pdgmtype2 = (tagDGMType2*)pdgm;
      
      // тут обработка датаграммы типа 2
      
      return rslt;
    }
    
    int DGMHndlr3(void *pdgm)
    {
      int rslt = 0;
      tagDGMType3 *pdgmtype3 = (tagDGMType3*)pdgm;
      
      // тут обработка датаграммы типа 3
      
      return rslt;
    }
    
    int ((*DGMHandler[3]))(void *pdgm) = {DGMHndlr1, DGMHndlr2, DGMHndlr3};
    
    int DGMDispatch(void *pdgm)
    {
      tagDGMHeader *pdgmhdr = (tagDGMHeader*)pdgm;
      
      // тут какие-то проверки, валидации и т.п., если надо
      
      return DGMHandler[pdgmhdr->type](pdgm);
    }

    Ну как-то так, весьма схематично. Работает быстро (это критично), легко масштабируется - появился новый тип датаграммы - пишем обработчик, добавляем в таблицу и вуаля.

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


    1. Tsvetik
      04.06.2022 18:23

      Например, массивы ссылок не существуют


      1. F0iL
        04.06.2022 23:11

        Можно сделать массив std:: reference_wrapper'ов. У них нет дефолтного конструктора, но если массив явно-инициализируемый или динамический, то это не проблема.


  1. MrKirushko
    06.06.2022 07:23
    +1

    Ну, как по мне if(!data.has_value()) ничем не лучше if(nullptr == data) ни в плане скорости исполнения, ни по расходу памяти, ни в плане восприятия кода человеком никаких преимуществ я не вижу. Как, впрочем, и вообще в минимум 90% того что предоставляет STL (а из оставшихся 10% проще либо Qt либо вообще стандартной библиотекой пользоваться). Я лично так вообще обычно пишу if (!data)- и нажатий на кнопки надо меньше и на С без изменений работает.


  1. AlexXZero
    06.06.2022 07:24
    -1

    Используйте ссылки вместо указателей

    Похоже у автора всё смешалось воедино. Ссылки - это не замена указателям, это тип данных для хранения адреса объекта, и этот адрес нельзя изменить. Но их НЕЛЬЗЯ просто так использовать вместо указателей. Если уж на то пошло, то почему тогда автор не проверяет ссылки на nullptr? Ведь вызывающий функцию код может сделать что-то типа:

    int *a = nullptr;
    void f(const int &a);
    ...
    f(*a);

    Именно поэтому прежде чем вызывать функцию надо читать её описание, а какие данные она готова принимать, если она принимает указатель, то это ещё не значит, что можно туда nullptr запихивать. Я бы сказал, что по умолчанию в C++ этого делать как раз таки нельзя.

    С другой стороны я поддерживаю идею, что указатель как аргумент функции в C++ - это плохой тон. А вот внутри кода - пожалуйста используйте указатель, как замену ссылкам, т.к. их можно модифицировать и хранить указатель на текущий элемент дерева, и т.п. Но всё равно предпочтение должно отдаваться ссылкам, если вам не нужно менять их значение.


    1. mayorovp
      06.06.2022 10:17

      Если уж на то пошло, то почему тогда автор не проверяет ссылки на nullptr? Ведь вызывающий функцию код может сделать что-то типа [...]

      Согласно стандарту языка, ссылка никогда не указывает на nullptr, а разыменование nullptr — это UB.


  1. kmatveev
    06.06.2022 07:45
    +1

    Заголовок и вступление ставят под сомнение полезность указателей. Но вся статья рассматривает только применение указателей как параметров.


  1. holydel
    06.06.2022 12:39

    Блин, с таким названием статьи ожидал на КДПВ что то вроде этого: