Pull to refresh

Comments 29

Дополнения к std::shared_ptr и std::make_shared:
1. Без объяснения как на практике устроен std::shared_ptr, невозможно понять что за сценой присутствует control block и без std::make_shared у нас появиться дополнительный уровень косвенности и следственно лишнее выделение памяти под него. std::make_shared объединяет эти два выделения, делая создание и удалении быстрее.
2. std::weak_ptr естественно делает увеличение счетчика ссылок, но это отдельный счетчик, который также находится в control block-е.

Отсюда и один минус make_shared: если кусок памяти большой, и счётчик shared_ptr нулевой, но есть weak_ptr, то память мы не вернём, только объект разрушим. Иногда это критично и стоит знать и учитывать.

Всё верно. Это обратное следствие. Как всегда дилемма: скорость-память.

Также ещё из минусов make_shared: custom deleter не задать, aliasing constructor не вызвать.

Можете проиллюстрировать свой пример кодом?
Я не совсем понимаю как такая ситуация возможна. Если счётчик ссылок в shared_ptr равен нулю, то это значит, что деструктор объекта вызван, объект разрушен и память возвращенна. weak_ptr не увеличивает тот счётчик ссылок, на котором базируется shared_ptr когда решает удалить объект. Это, как я понял из статьи, значит, что weak_ptr вполне может указывать на объект уже удалённый shared_ptr, но это нормально.

Я писал, писал, потом нажал на (?) и текст без предупреждения канул в лету :(

Вызов деструктра != освобождения памяти. Посмотрите на placement new, ручной вызов деструктора. Как раз оно и используется в shared_ptr.

При обычном создании shared_ptr аллоцируется два блока памяти: под данные и под контрольный блок, контрольный блок живёт пока есть хоть один клиент: shared_ptr и/или weak_ptr. Благодаря этому блоку, weak_ptr понимает, что use_count равен нулю и данные невалидны создать shared_ptr нельзя.

Минус: фрагментация памяти.

make_shared выделяет (точнее не совсем он, но это не принципиально) один блок памяти, в котором располагается как контрольный блок, так и данные. Когда последний shared_ptr будет разрушен, то будет вызван деструктор объекта, но память (считаем что у нас есть как минимум один weak_ptr), не будет освобождена, так как иначе станет невалидным контрольный блок. И, собственно, как только последний weak_ptr разрушится, тогда разрушится и контрольный блок и весь блок памяти будет освобождён.

Как-то так.

У make_shared/make_unique есть ещё одна проблема. Нельзя создать экзепляр некоторого класса A, в области видимости этого класса, если конструктор этого класса находится в protected или private области:

class A
{
public:
  static std::shared_ptr<A> Create()
  {
    return make_shared<A>(); // Ошибка.
  }
  
protected:
  A() = default;
};

Тоже самое со static_pointer_cast и dynamic_pointer_cast.

Я бы ещё добавил рекомендацию рассматривать std::unique_ptr как умный указатель "по-умолчанию". Неоднократно видел неоправданное использование std::shared_ptr.

Давайте будем честными - все мы видели очень мало оправданного использования std::shared_ptr :)

Указатель — это просто адрес ячейки памяти. Он может указывать в принципе на всё. Не важно, существует это или нет, что по этому адресу лежит если есть и т.д.

Только не C++
struct A {
	int x, y:1;
	void mf() {}
	virtual void vmf() {}
	static  void sf() {}
	A() {}
	~A() {}
};

int main() {
	A a, aa[2];
	auto lf1=[&](){ a.x=1; };
	auto lf2=[&](){ a.x=2; };
	label:

	auto p01=&a;
	auto p02=&1[aa];
	auto p03=&a.x; 
	// auto p04=&a.y;   // attempt to take address of bit-field

	auto p05=&A::x;
	// auto p06=&A::y;  // invalid pointer to bit-field
	auto p07=&A::sf;
	auto p08=&A::mf;
	auto p09=&A::vmf;
	// auto p10=&A::A;  // taking address of constructor
	// auto p11=&A::~A; // taking address of destructor
	// auto p12=&label; // was not declared in this scope

	auto p13=&lf1;
	p13=&lf1;
	// p13=&lf2;// cannot convert ‘main()::<lambda()>*’ to ‘main()::<lambda()>*’ 
	return 0;
}

А еще есть отдельный тип UB указателей не представимых в машинных кодах :)

Часть из того, что вы написали, это не репрезентативные примеры.


Указатель на bit field невозможен просто потому, что bit field меньше, чем минимальный размер ячейки памяти, т.е. 1байт. Это не противоречит утверждению о том, что "указатель — это просто адрес ячейки памяти". Но теоретически можно получить указатель на область памяти, где будет храниться этот bit field. Указывать он туда будет, но никто не говорит, что это будет полезный указатель.


Указатель на конструктор — хороший пример, однако, технически такой указатель есть. В конце концов, конструктор вызывается из ::new. Сложность тут в том, что чтобы его использовать, нужно уметь выделять память для объекта, указатель на который нужно потом передать конструктору, без вызова конструктора. С деструкторами скорее всего похожая ситуация.


Указатель на label получить можно, но есть нюанс.


С лямбдами в принципе проблем никаких, это просто разные функции, с разными типами. То что в ошибке указаны одинаковые сигнатуры — это проблема сообщений об ошибки, такие лямбды могли бы быть названы как main()::<lambda()>[1] и main()::<lambda()>[2]. Проблемы не будет, если capture list отсутствуют, скорее всего. Но не уверен.
Но это на самом деле не важно с позиции указателей, потому что проблемы с существованием указателей нет.
Если задать void * p13 вместо auto, ошибка исчезает.

Указатель на bit field невозможен просто потому, что bit field меньше, чем минимальный размер ячейки памяти, т.е. 1байт

Да хоть 32бита можно сделать, всё равно нельзя: wandbox.org/permlink/AAsG87xzqrrBXQmG

И что по поводу p05,p08,p09, которые уже не просто адрес ячейки памяти.

ps: &&label поддерживают не все компиляторы.

pps: я к тому что единообразия нет.

А что вот это такое?
auto p02=&1[aa];

Это просто адрес элемента массива aa с индексом 1. Тоже самое что (aa+1) или &(aa[1]).

std::unique_ptr<Object> p3 = std::make_unique<Object>("Lamp");

Зачем там длинно писать и указывать тип два раза, когда make_unique уже идёт с типом.

auto p3 = std::make_unique<Object>("Lamp");

Вроде проблема с утечкой памяти была исключена ещё в C++17.
Единственная причина использовать make_shared это экономия ресурсов (и то не всегда). А в чём преимущества make_unique?

В удобной семантике и консистентности с make_shared? Ну и просто более красиво как-то смотрится, чем

auto ptr = std::unique_ptr{ new Object };

Мне лично проще думать "make unique Object", чем "construct unique_ptr which holds Object".

В каком смысле проблема с утечкой была решена? Циклические ссылки в рамках подсчёта ссылок решить нельзя(без костылей)

Мой коммент относился к:
void function(std::unique_ptr<A>(new A()), std::unique_ptr<B>(new B())) { ... }
"Предположим, что new A() выполняется успешно, но new B() выбрасывает исключение: вы ловите его, чтобы возобновить нормальное выполнение программы. К сожалению, стандарт C++ не требует, чтобы объект A был уничтожен, а его память высвобождена: память тихо утекает, и нет способа ее очистить."
IMHO в C++17 компилятор не имеет право перемешивать этапы инициализации параметров ф-ии.
"In a function call, value computations and side effects of the initialization of every parameter are indeterminately sequenced with respect to value computations and side effects of any other parameter." [ Order of evaluation - cppreference.com ] Поэтому описанная утечка памяти не произойдёт и использование make_shared/make_unique в этом случае не обязательно.

А, да, это правда. Ну в интернете устарешей информации даже про с++11 ещё лет на 20 хватит

Ещё хочу напомнить про незаслуженно забытый aliasing constructor для shared_ptr.
Это мегафича позволяющая shared_ptr-у ссылаться и владеть совершенно разными объектами! Часто требуется передать указатель на часть чего-то большего, и при этом гарантировать, что это большее не удалится, пока мы работаем с его частью, shared_ptr позволяет это.

Бывает надо передать в shared_ptr указатель без права владения. В этом случае тоже помогает aliasing constructor, но не уверен, что здесь нет нарушения какого-нибудь правила стандарта.
Может кто прокомментировать следующий код?

#include <cstdio>
#include <memory>

void foo(std::shared_ptr<int> sp) {printf("*sp = %d\n", *sp);}

int main()
{
    int n = 777;
// redundantly:	std::shared_ptr<int> sp{ &n, [](int*) {} };
    std::shared_ptr<int> sp{ std::shared_ptr<void>{}, &n };

    foo(sp);
}

https://godbolt.org/z/x6n6aGrxc

Вы совсем забыли, что даже unique_ptr имеет ненулевую стоимость и это напрягает. Источник: "There are no zero cost abstractions" by Chandler Carruth, CppCon

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

Любые указатели поддерживают динамический полиморфизм (т.к. могут указывать на объекты переменного размера) и гарантировано дешево копируются.

стековый объект из функции наружу передать без копирования не получится, а динамический объект, на который указывает unique_ptr, - можно выдать без копирования:

class CObject {};
std::unique_ptr<CObject> makeObject()
{
    auto obj = std::make_unique<CObject>();
    return obj;
}

Подскажите можно ли (и если да, то как) использовать умные указатели с Direct2D?
Пример кода без умных указателей:

ID2D1Factory *D2DFactory; // Фабрика DirectX 2D  
ID2D1HwndRenderTarget* rt; // Экранный RenderTarget  
...  
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &D2DFactory);
D2DFactory->CreateHwndRenderTarget(  
  D2D1::RenderTargetProperties(),  
  D2D1::HwndRenderTargetProperties(hwnd, size), &rt);  
...  
// использование DirectX 2D  
...  
SafeRelease(&rt); 
SafeRelease(&D2DFactory);

Sign up to leave a comment.