Comments 57
Вот только std::optional<int&> невалиден, так что как минимум для этого указатели всё-ещё нужны
А зачем нам опциональная ссылка? Ссылка ведь - это гарантированное наличие значения. А тут мы поверх него вешаем, что значения может не быть...
std::optional<std::shared_ptr<int>> ?
Дополню ещё 2 места, где без указателей никак:
Поля типа Foo* в классе, если требуется менять значение этого поля. Референсы менять нельзя.
shared_ptr и unique_ptr - там где, в деструкторе нужно удалять другие объекты. Если использовать референсы, то придётся руками писать удаление в деструкторе.
невалиден, как и во всем stl, впрочем тут нужен не указатель, а std::reference_wrapper<>
Увы, есть много библиотек, фреймворков и движков в которых инициализация объектов - это не одномоментное действие выполнимое в конструкторе. Поэтому если у этих объектов есть зависимости от других объектов реализовать поля для связи с зависимостями в ссылочных типах (которые обязаны инициализироваться на конструкторе) невозможно
Как бы, нет. Ваш пример некорректен.
Если у вас значения в других потоках меняются, то тут не в указателях дело, а нужно многопоточную синхронизацию предусматривать. Тут хоть контейнеры, хоть умные указатели делай - ничего не поможет без межпоточной синхронизации. Так что мимо.
Со второй придиркой аналогичные проблемы. Если вы получаете указатель а он где-то во внешнем коде освобождается пока функция не завершилась, то проблема не с указателями. Так у вас и значения в контейнерах могут измениться, и контейнеры освободиться. Указатели тут ни при чём
С ног на голову перевернуто в статье. Указатели не для того, чтобы 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 aT*
: the caller can't accidentally pass in something utterly bogus (e.g. anint
converted to aT*
), 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*
orT&
arguments rather than smart pointersReason
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 можно узнать существует объект или нет, а у обычного указателя такой обратной связи нет.
А в C++ нельзя передать в функцию ссылку на smart pointer?
Я же с этого и начал: What about passing or returning a smart pointer by reference?
Ну вот эта часть непонятна:
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);
}
С умными указателями так сделать(без костылей) нельзя. Потому что, как там и сказано, умные указатели помимо владения ещё и управляют аллокацией.
Можно, но это может быть не оптимально.
Например при передаче ссылки на умный указатель необходимо этот самый указатель сохранить в памяти и передать ссылку на этот участок памяти.
Это мешает некоторым оптимизациям компилятора.
А можно немного подробнее про то, как реактивные преобразования избавляют от указателей в никуда?
Или - где про это почитать?
дерево тоже на умных указателях писать?
Я также видел случаи, когда проверка на
nullptr
в функции была опущена попросту потому что было сложно решить, что делать в случаеnullptr
. Например, когдаnullptr
получает функция, которая возвращаетvoid
.
Это лучшее оправдание говнокода которое я видел. Нормально подогнать сигнатуру функции под код, но не код под сигнатуру, который ещё и работать будет с вероятностью 50/50 при этом.
Пример из жизни. У товарища банкомат не принимает карту, я ему говорю, ладно, забей. Ну он он и забил. Буквально вдавил в картоприёмник. Неделю стрелял денег пока инкассация не прошла.
Я также видел случаи, когда проверка на
nullptr
в функции была опущена попросту потому что было сложно решить, что делать в случаеnullptr
. Например, когдаnullptr
получает функция, которая возвращаетvoid
.
А в чем проблемам:
std::cerr << "null pointer'' << std::endl;
throw;
Кроме того указатели понятны и естественны для большинства программистов, а std::optional выглядит как китайская грамота. Код же мы не только для себя пишем.
В С++ есть ссылки на массивы и это очень удобно
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 }
Это ж сарказм был, правда?
Нет, не сарказм. Действительно очень удобно.
А чем std::vector не устраивает?
Ну так это уже от реализации вектора на уровне компилятора зависит, какую память он будет использовать. Теоретические ничего не запретит аллоцировать вектор на стеке, например. Единственное ограничение вектора by design, насколько помню, так это участок памяти должен быть сплошным.
А если вектор совсем использовать не хочется, так для этого C-массивы существуют. Но они не динамические.
А как сделать динамический вектор на стеке? Предположим, выделили на стеке сколько-то байт под вектор, заполнили значениями, затем еще что-нибудь на стеке выделили, а тут решили увеличить размер вектора. И куда ему расти? Вариант только выделить на стеке новый кусок под новый стек и скопировать в него старый вектор и добавить новое значение. Правда, освободить память "старого" вектора не получиться. Как-то не экономно, как по мне.
Вариант только выделить на стеке новый кусок под новый стек и скопировать в него старый вектор и добавить новое значение.
Так "классический" std::vector тоже самое делает, только в хипе. Конечно, можно преаллоцировать кусок памяти, и до определённых размеров не выделяться заново. Но ведь и ссылка на вектор данную проблему не решает. А выглядит вместе с темплейтами коряво (см исходный пост). То есть, вопрос, какая решалась проблема в итоге? :)
Так "классический" std::vector тоже самое делает, только в хипе.
Так я об этом и говорю. Из-за того что вектор выдялет в куче, он может как выделить, так и освободить память. В случае стека освободить память не получится и она останется мертным грузом.
А причём тут ссылка на вектор, когда обсуждается ссылка на массив постоянного размера?
А если тело функции не хедере, а в другом исходном файле?
Что мешает Благородному Дону не использовать указатели в своем коде, но использовать только ссылки?
Вот я сейчас много пишу на достаточно специфическом языке 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);
}
Ну как-то так, весьма схематично. Работает быстро (это критично), легко масштабируется - появился новый тип датаграммы - пишем обработчик, добавляем в таблицу и вуаля.
Так что без указателей скучно. Но применять с осторожностью и только там, где это реально дает какой-то профит. Но в большинстве случаев вполне пригодны ссылки.
Ну, как по мне 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++ - это плохой тон. А вот внутри кода - пожалуйста используйте указатель, как замену ссылкам, т.к. их можно модифицировать и хранить указатель на текущий элемент дерева, и т.п. Но всё равно предпочтение должно отдаваться ссылкам, если вам не нужно менять их значение.
Заголовок и вступление ставят под сомнение полезность указателей. Но вся статья рассматривает только применение указателей как параметров.
Блин, с таким названием статьи ожидал на КДПВ что то вроде этого:
Долой указатели