Przekazywanie inteligentnych wskaźników i noexcept

0

Nie potrafię zrozumieć zasad przekazywania inteligentnych wskaźników, czy mógłby mi ktoś to lepiej wyjaśnić? Głównie zależy mi na łopatologiczny wyjaśnieniu, kiedy shared_ptr powinnien być przekazywany przez wartość, a kiedy przez referencję, const referencję.
W kodzie znajdują się pytania.

Czy zawsze dajecie noexcept jeśli funkcja jest noexcept? Np. getter, setter czy tylko jeśli da to wzrost wydajności np. move semantic.
Co w przypadku desktruktora, zawsze noexcept? Co jeśli destruktor z jakiegoś dziwnego powodu korzysta z operatora delete, czy wtedy też dajecie noexcept? Teoretycznie delete jest noexcept, ale delete ptr; delete ptr; to UB i wtedy może być rzucany wyjątek. W takim razie czy jeśli funkcja potencjalnie wywołuje UB to czy jest noexcept?

Kod przykładowy do pytań o pointery:

#include <iostream>
#include <memory>

class Foo
{
public:
    Foo() = default;
    Foo(const std::shared_ptr<int>& ptr) : ptr(ptr){} // Ptr w klasie jest drugim właścicielem. Dobrze? 
    Foo(std::shared_ptr<int> ptr) : ptr(std::move(ptr)){} // Czy tak?
    void print() const
    {
        std::cout << x << '\n';
    }
    void change() noexcept // noexcept?
    {
        x = 5;
    }
private:
    int x = 3;
    std::shared_ptr<int> ptr;
};

void useUniquePtr(std::unique_ptr<Foo> ptr) // Funkcja staje się właścicielem i usuwa wskaźnik
{
    ptr->change();
    ptr->print();
}

void useUniquePtr(const std::unique_ptr<Foo>& ptr) // Funkcja chce użyć wskaźnik, ale może też nie użyć
{
    if(ptr)
    {
        ptr->change();
        ptr->print();
    }
}
// w innych przypadkach użycie powinno się użyć shared_ptr


void useSharedPtr(const std::shared_ptr<Foo>& ptr) // Funkcja chce użyć wskaźnik, ale może też nie użyć
// Jak ma wyglądać ciało funkcji, tak, czy tak jak niżej?
{
    try
    {
        if(ptr)
        {
            ptr->change();
            ptr->print();
        }
        else
        {
            throw("Co za dzban usunął pointer, bug w kodzie\n");
        }
    }
    catch(const std::string& ex)
    {
        std::cout << ex;
    }

}

void useSharedPtr(std::shared_ptr<Foo> ptr) // nowy współwłaściciel
{
    ptr->change();
    ptr->print();
}
int main()
{
    auto unique = std::make_unique<Foo>();
    auto shared = std::make_shared<Foo>();
    return 0;
}

9

Mogę być nieco "zbiasowany" w kierunku tego jak piszę sam, czy to w pracy czy we własnych projektach. No jedźmy, shared_ptr najsampierw:

przez wartość

zawsze kiedy chcesz zwiększyć refcount. Ew. robisz "sink argument" i exception safety niekoniecznie Cię obchodzi.

a kiedy przez referencję

Jeżeli pytasz o rvalue rf (&&) to kiedy chcesz oddać własność bez zmiany refcountu. Przez lvalue ref (&) nie bardzo widzę sens, chyba, że ma to być parametr wyjściowy, w stylu:

bool remove_from_queue(shared_ptr<HeavyObj>& out) noexcept
{
  if (mQueue.empty()) return false;
  out = move(mQueue.front()); //albo swap
  mQueue.pop();
  return true;
}

const referencję

Nie bardzo widzę sens.

Czy zawsze dajecie noexcept jeśli funkcja jest noexcept? Np. getter, setter czy tylko jeśli da to wzrost wydajności np. move semantic.

Musisz wyczuć. Hash, swap, move assignment/constructor mają sens, no bo wyobraź sobie coś takiego: bierzesz wartość z jednego obiektu, wsadzasz w drugi, leci wyjątek, obiekt 1 jest wewnętrznie popsuty, obiekt 2 nie powstał. Lipa trochę, nawet jak tego exceptiona złapiesz, nie odtworzysz stanu obiektu w prosty sposób.

Co w przypadku desktruktora, zawsze noexcept

Nie trzeba, od C++11 to jest implikowane by default. Chyba, że z jakiegoś powodu chcesz z niego rzucić, wtedy musisz zrobić noexcept(false).

W takim razie czy jeśli funkcja potencjalnie wywołuje UB to czy jest noexcept?

Jeżeli funkcja ma UB, to noexcept to Twój najmniejszy problem i rzucanie nie ma tu nic do rzeczy ;)

Co do Twojego kodu (komentarze dotyczą snippetów pod nimi):
ja bym zrobił tak, chyba, że profilujesz i ten jeden move Ci ciąży

    Foo(std::shared_ptr<int> ptr) : ptr(std::move(ptr)){}

To jest imho ok. Alternatywnie możesz przekazać przez rvalue ref jeżeli przed operacjami na wksaźniku dzieje się coś co może rzucić, wtedy wskaźnik ma szanse nie zostać zniszczony.

void useUniquePtr(std::unique_ptr<Foo> ptr) // Funkcja staje się właścicielem i usuwa wskaźnik
{
    ptr->change();
    ptr->print();
}

Tak samo dobrze wygląda to:

void useSharedPtr(std::shared_ptr<Foo> ptr)

Natomiast imho poniższe ma mały sens. Przekaż przez goły wskaźnik Foo* i z głowy.

void useUniquePtr(const std::unique_ptr<Foo>& ptr);

Podobnie

void useUniquePtr(const std::unique_ptr<Foo>& ptr) // Funkcja chce użyć wskaźnik, ale może też nie użyć

EDIT: odn. noexcept jeszcze: generalnie to się zachowuje (a czasem nawet kompiluje do czegoś takiego) jak +- taki kod:

void doStuff() noexcept
{
  doStuffImpl();
}
/* zachowa jak kod poniżej */
try {
  doStuffImpl();
} catch(...) {
  std::terminate();
}

no i teraz musisz Sobie odpowiedzieć (co nie musi być proste! ;)): kiedy nie ma sensu wyjątku łapać i będzie on raczej na tyle poważny, że lepiej, żeby aplikacja "sama sobie dała w mordę"? Move/swap/hash - mają sens (ale mogą być też noexcept(false) jeśli jednak zamierzasz to obsługiwać). Destruktory - raczej też. Setter - a diabli wiedzą, jak wyjątek poleci od walidacji pola to raczej nie - przynajmniej moim zdaniem, może nawet mieć sens go złapać i jakoś mądrze obsłużyć. Jednocześnie, jeśli poleci bad_alloc to chyba lepiej dać aplikacji umrzeć. Pytaniem osobnym jest kiedy, tzn. czy złapać go możliwie wcześnie i ręcznie dokonać harakiri wołając samemu abort/terminate lub coś w tę mańkę, czy może dać wyjątkowi (niech to będzie ten bad_alloc) przewiercić stos i ubić aplikację "u góry"; no ale to jest totalnie osobny temat.

EDIT2:
Na kija w tym rzucać i zaraz łapać? Albo to rzucasz i wypuszczasz na zewnątrz (ale wtedy raczej nie stringa) albo po prostu logujesz.. BTW, rzucasz const char*, próbujesz łapać const std::string&, to się nie uda ;)

void useSharedPtr(const std::shared_ptr<Foo>& ptr) // Funkcja chce użyć wskaźnik, ale może też nie użyć
// Jak ma wyglądać ciało funkcji, tak, czy tak jak niżej?
{
    try
    {
        if(ptr)
        {
            ptr->change();
            ptr->print();
        }
        else
        {
            throw("Co za dzban usunął pointer, bug w kodzie\n");
        }
    }
    catch(const std::string& ex)
    {
        std::cout << ex;
    }

}
4
void useUniquePtr(const std::unique_ptr<Foo>& ptr)
{
    if(ptr)
    {
        ptr->change();
        ptr->print();
    }
}

To nie ma sensu, jak chcesz tylko użyć i nie przekazywać zasobu to przekaż przez stałą referencję co również wyklaruje Ci w którym miejscu będziesz musiał sprawdzić czy wskaźnik pokazuje na zasób

void useUniquePtr(const Foo& foo)
{
     foo.change();
     foo.print();
}

//.....................................

if(auto ptr = create())
    useUniquePtr(*ptr);


1

Warto zauważyć, że funkcję

void useUniquePtr(std::unique_ptr<Foo> ptr) // Funkcja staje się właścicielem i usuwa wskaźnik
{
    ptr->change();
    ptr->print();
}

można wywołać tylko używając ** std::move**

useUniquePtr( move(unique) );

Nie można utworzyć std::unique_ptr przez wartość - nie jest kopiowalny, można tylko go przenieść.

3
Cepo napisał(a):

Teoretycznie delete jest noexcept, ale delete ptr; delete ptr; to UB i wtedy może być rzucany wyjątek. W takim razie czy jeśli funkcja potencjalnie wywołuje UB to czy jest noexcept?

To zamieszanie wynika, z konfliktu nazewnictwa. Wyjątek systemowy nie ma nic wspólnego z wyjątkami C++, poza nazwą.
Undefined Behavior w C++ to po prostu "zachowanie niezdefiniowane", może się stać cokolwiek "demony mogą wylecieć ci z nosa". Jeśli masz szczęście taki błąd w programie doprowadzi do błędu programu, który system zinterpretuje jako nieodwracalny błąd i od razu zakończy proces programu (bez wykonywania jakiegokolwiek kodu programu - nie do końca prawda, ale nie chcę wchodzić w szczególny techniczne przydatne raz na milion programów).
Teraz na Windows to zachowanie zostało nazwane wyjątkiem, ale nie ma nic wspólnego z wyjątkiem C++ ergo noexept nie ma żadnego przełożenia na to zachowanie.
Na innych systemach, to zachowanie nazwane jest crash (wypadek/katastrofa), albo panic (panika), albo ... .

0

@alagner: Zmieniłeś moje postrzeganie noexcept, wcześniej rozumiałem to tak, że dajemy noexcept, gdy funkcja nie rzuca wyjątkiem, a teraz rozumiem, że noexcept powinnien być tam, gdzie rzucenie wyjątku jest niedopuszczalne.

Przykładowo tutaj, noexcept nic nie zmienia?

void foo() noexcept
{
    puts("hello"); // puts nie rzuca wyjątkiem, bo jest z C
}

Natomiast tak jak wspomniałeś, przy swapie nie ma mowy o wyjątku, spowoduje on niedopuszczalną sytuację i wtedy warto dać noexcept, bo to przyspieszy program.
Ale pojawia się inny problem, załóżmy że mój projekt ma 500k linii kodu i nagle jedyne co się pojawia to core dumped, czy w takiej sytuacji ten wyjątek nie byłby zbawienny? Z drugiej strony czy nierzucony wyjątek nie jest zero-cost?

1

Fajny artykuł wyjaśniający wypisujący kilka punktów Cpp Core Guidelines w tej materii.
Po dokładniejszym przeczytaniu, lepiej tylko skorzystać z linków w tym artykule i czytać Cpp Code Guidelines.

1 użytkowników online, w tym zalogowanych: 0, gości: 1