LINUX.ORG.RU

Почему так сделали в std::unique_ptr?

 


0

3
#include <iostream>
#include <memory>
#include <vector>
#include <thread>

using namespace std;

class MyClass{
public:
    MyClass(){
        cout << "Created " << this  << " at " << std::this_thread::get_id()  << endl;
    }

    void SayHi(){
        cout << "Say hi from thread " << std::this_thread::get_id() << endl;
    }

    virtual ~MyClass(){
        cout << "Destroyed " << this << " at " << std::this_thread::get_id() << endl;
    }
};

void Use(std::unique_ptr<MyClass> m){
    m->SayHi();
}

int main(){
    auto m = std::unique_ptr<MyClass>(new MyClass());
    
    cout << std::boolalpha;

    // Prints true
    cout << "is null " << static_cast<bool>(m) << endl; 

    m->SayHi();

    std::thread t1(Use, std::move(m));

    // Prints false
    cout << "is null " << static_cast<bool>(m) << endl; 
    
    // WTF? this works?!?!?
    m->SayHi();

    // Thank you C++ designers that I at least check...
    if(m)
        m->SayHi(); 
    }

    t1.join();
}

Почему unique_ptr остается юзабельным после std::move? Это считается хорошим дизайном? Кому нужен такой сценарий?

Я бы вот хотел чтобы в том вызове оно или не скомпилировалось или упало в рантайме чтобы рано найти ошибку. А так при передаче указателя в другой поток никакой речи о thread safety быть не может. Можно возразить что unique_ptr не должен решать эту проблему. Но это еще вопрос, а почему это не должен?

★★★★★

Последнее исправление: vertexua (всего исправлений: 2)

Ты же можешь написать такое в плюсах

MyClass *my = nullptr;

my->SayHi();
Это не проблема unique_ptr, он просто поддерживает консистентность с остальными плюсами.

mashina ★★★★★
()
#include <iostream>

struct A {
    void f() { std::cout << "f" << std::endl; }
};

int main() {
    {
        A a;
        a.f();
    }
    {
        A * a = nullptr;
        a->f();
    }
}
$ g++ main.cpp -o main -std=c++11
$ ./main 
f
f

Что в твоём коде, что здесь UB, но поведение таки одинаковое. Было бы странным, если бы поведение различалось.

devsdc ★★
()
Ответ на: комментарий от yoghurt

Совсем UB. a->b === (*a).b === «разыменование нулевого указателя» === UB. Это работает для не виртуальных методов (пока они не станутвиртуальные... ну а потом выстрелит, когда-нибудь), и применяется нехорошими людьми сознательно, но это UB. Компилятор имеет право соптимизировать явный вызов метода на нулевом указателе скажем в std::terminate() и будет прав, по стандарту (лень снова искать те цитаты).

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 1)

Я тут еще поговорил, сказали что иначе в оператор стрелку пришлось пихать if и указатель не был бы zero cost. Или делать какой-то reset и полагаться на сегфолт. Это более адекватно, но не сделали

vertexua ★★★★★
() автор топика
Ответ на: комментарий от xaizek

a->b === (*a).b === «разыменование нулевого указателя»

Это в общем случае. Мы-то говорим про конкретный код, не?

Это работает для не виртуальных методов

...которые не обращаются к this. Вот при обращении к this уже и будет «разыменование нулевого указателя».

А вообще ссылка на главу стандарта тут не помешала бы, самому уже интересно :)

yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

Мы-то говорим про конкретный код, не?

#include <iostream>
using namespace std;

class Foo {
public:
	void foo() {
		cout << "What?! " << endl;
	}
};

int main() {
	(*((Foo*)0)).foo();
	return 0;
}

Почему это работает? У указателей переопределена операция *?

Tanger ★★★★★
()

Почему unique_ptr остается юзабельным после std::move?

Потому что сделать лучше в рамках Си++ невозможно.

tailgunner ★★★★★
()
Ответ на: комментарий от vertexua

nullptr там и так есть. Сегофолт без повода образоваться не может.

mashina ★★★★★
()
Ответ на: комментарий от Tanger

Почему это работает?

Потому что компилятор вызывает Foo::foo() напрямик с нулевым this. Вот это:

(*((Foo*)0))

Возможно выкинулось из-за оптимизации.

У указателей переопределена операция *?

wut

yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

Это в общем случае. Мы-то говорим про конкретный код, не?

Не. Стандарт описывает все случаи, работает оно на честном слове из-за особенностей реализации.

Прямым текстом этого не сказано, две части, мол a->b == (*a).b, а разыменовывать надо только то, что указывает на объект. И всё равно, что это делает компилятор, он как бы формирует временную ссылку после разименования указателя.

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

5.2.5, 2:

Class member access
...
The expression E1->E2 is converted to the equivalent form (*(E1)).E2; the remainder
of 5.2.5 will address only the first option (dot).

5.3.1, 1:

Unary operators
...
The unary * operator performs indirection: the expression to which it is applied
shall be a pointer to an object type, or a pointer to a function type and the
result is an lvalue referring to the object or function to which the expression points.

xaizek ★★★★★
()
Ответ на: комментарий от yoghurt

Операция *((Foo*)0) ни во что не транслируется. Разыменование не генерирует инструкций. Вот обращение к тому, что получилось — да. В случае вызова метода указатель на объект кладётся первым параметром и всё, т. е. в коде (*((Foo*)0)).bar() на самом деле разыменования (обращения по адресу 0 на чтение или запись) не происходит.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 1)

ну и что тут такого? вызов с нулевым this. Отбратитсь в этом вызове к члену класса (например какой-нить std::string) - получишь сегфолт.

anonymous
()
Ответ на: комментарий от Tanger

потому что твой код эквивалентен

#include <iostream>
using namespace std;

class Foo {};

void foo(Foo *f) {
	cout << "What?! " << endl;
}

int main() {
	foo(nullptr);
	return 0;
}
Stil ★★★★★
()
Ответ на: комментарий от yoghurt

По стандарту нужны, даже если это «гипотетическое» разименование. Возможно, есть архитектуры, где в this при вызове что-то складывается, там подобные трюки работать не будут. Использовать нулевой this безусловно «элегантное» в некотором роде решение, но при этом оно же и broken by design.

xaizek ★★★★★
()
Ответ на: комментарий от anonymous

Хммм, что то я это не учел. Спасибо

vertexua ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

Ну ты просто написал, что «выкинулось из-за оптимизации», вот я и уточняю — не из-за оптимизации, а вообще по факту никакого кода там не генерируется.

intelfx ★★★★★
()

Хорошо, после того как обсудили почему нельзя пофиксить в рантайме, так почему не сделали костыль на уровне компилятора, завершающий scope уже на этапе std::move, который не позволял компилировать использования после move

vertexua ★★★★★
() автор топика

Напиши сам однопоточную версию shared_ptr, unique_ptr. Затем, используя возможности параметрического полиморфизма в c++, напиши многопоточную реализацию (hint: use policy). О policy, traits, списках типов и compile-time полиморфизм написано хорошо в Alexandrescu Modern C++ Design.

Т.к. unique_ptr пришёл в стандарт из boost, а boost написан в общем под влиянием Andrei Alexandrescu, то советую хотя бы бегло ознакомиться.

nerdogeek
()
Ответ на: комментарий от yoghurt

Разве чтобы выковырять поинтер начать им управлять руками. Заменять на другой - не нужно

vertexua ★★★★★
() автор топика
Ответ на: комментарий от vertexua

Хорошо, после того как обсудили почему нельзя пофиксить в рантайме, так почему не сделали костыль на уровне компилятора, завершающий scope уже на этапе std::move, который не позволял компилировать использования после move

В runtime'е пофиксить можно, но не нужно. std::move() ничего с объектом не делает, а move semantic оставляет после себя валидный объект и вполне нормально дальше продолжать его использование. Ты опять ищешь проблему не там, компилятор мог бы предупреждать о потенциальных разименованиях nullptr, но для этого он должен быть достаточно «умным».

mashina ★★★★★
()
Ответ на: комментарий от nerdogeek

Т.к. unique_ptr пришёл в стандарт из boost

Ээээ... что? Может ты с shared_ptr путаешь? unique_ptr стал возможен только после переделки языка.

pathfinder ★★★★
()

std::thread t1(Use, std::move(m));

Ну так поэтому перемещение и явное. Ну а дальше уже твоя внимательность, в С++ такого много. Проверки увеличили бы стоимость абстракции, а так unique_ptr «всем лучше» обычно указателя. а то, что ты хочешь разве что в расте каком-то есть.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Я думаю что это и навеяно Rust-ом, как-то ожидалось что плюсовики сделают то же самое, но есть причины так не делать

vertexua ★★★★★
() автор топика
Ответ на: комментарий от vertexua

Я думаю что это и навеяно Rust-ом, как-то ожидалось что плюсовики сделают то же самое, но есть причины так не делать

В расте это на уровне языка сделано. Там ведь и возвращаемые значения нельзя не проверить на фейл. В С++ вряд ли будет.

DarkEld3r ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.