Pull to refresh

Comments 57

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

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

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

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

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

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

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

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

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

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

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

Референсы менять можно, для этого есть стандартный контейнер

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

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

UFO just landed and posted this here

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

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

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

UFO just landed and posted this here

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

Есть ещё один случай, когда 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.

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

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

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

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

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

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

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

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

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

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

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

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

void foo(int* p) {
  //something
}

void main() {
  int a = 42;
  foo(&a);
}

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

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

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

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

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

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

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

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

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

Потому что в С++ сырых указателей вообще быть уже не должно 10 лет как.

- для контекста

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

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

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

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

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

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

throw;

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

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

UFO just landed and posted this here

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

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

А как сделать динамический вектор на стеке? Предположим, выделили на стеке сколько-то байт под вектор, заполнили значениями, затем еще что-нибудь на стеке выделили, а тут решили увеличить размер вектора. И куда ему расти? Вариант только выделить на стеке новый кусок под новый стек и скопировать в него старый вектор и добавить новое значение. Правда, освободить память "старого" вектора не получиться. Как-то не экономно, как по мне.

 Вариант только выделить на стеке новый кусок под новый стек и скопировать в него старый вектор и добавить новое значение.

Так "классический" std::vector тоже самое делает, только в хипе. Конечно, можно преаллоцировать кусок памяти, и до определённых размеров не выделяться заново. Но ведь и ссылка на вектор данную проблему не решает. А выглядит вместе с темплейтами коряво (см исходный пост). То есть, вопрос, какая решалась проблема в итоге? :)

Так "классический" std::vector тоже самое делает, только в хипе.

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

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

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

PS. Кто-то против, что вектор аллоцируется на хипе? "Обоснуйте"

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

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

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

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

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

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

Вот я сейчас много пишу на достаточно специфическом языке 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);
}

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

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

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

UFO just landed and posted this here

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

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

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

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

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

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

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

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

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

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

Sign up to leave a comment.