Почему нет безопасной альтернативы unique_ptr :: operator * ()?

std::vector имеет функцию-член at() в качестве безопасной альтернативы operator[], так что применяется проверка границ и не создаются висячие ссылки:

void foo(std::vector<int> const&x)
{
const auto&a=x[0];     // What if x.empty()? Undefined behavior!
const auto&a=x.at(0);  // Throws exception if x.empty().
}

Тем не мение, std::unique_ptr не хватает соответствующей функциональности:

void foo(std::unique_ptr<int> const&x)
{
const auto&a=*x;       // What if bool(x)==false? Undefined behavior!
}

Было бы здорово, если бы std::unique_ptr была такая безопасная альтернатива, скажем член ref() (а также cref()), который никогда не возвращает висячую ссылку, а скорее выдает исключение. Возможная реализация:

template<typename T>
typename add_lvalue_reference<T>::type
unique_ptr<T>::ref() const noexcept(false)
{
if(bool(*this)==false)
throw run_time_error("trying to de-refrence null unique_ptr");
return this->operator*();
}

Есть ли веская причина, почему стандарт не предусматривает такого рода вещи?

17

Решение

Я подозреваю, что реальный ответ прост, и тот же самый для многих «Почему C ++ не такой?» вопросы:

Никто не предложил это.

std::vector а также std::unique_ptr не разрабатываются одними и теми же людьми в одно и то же время и не используются одинаково, поэтому необязательно следуйте одним и тем же принципам проектирования.

13

Другие решения

unique_ptr был специально разработан как легкий класс указателей с обнаружением нулевого состояния (например, указано в необязательный в Предложение добавить служебный класс для представления необязательных объектов (Редакция 3))

Тем не менее, возможность, которую вы спрашиваете, уже на месте, так как оператор * документация гласит:

// may throw, e.g. if pointer defines a throwing operator*
typename std::add_lvalue_reference<T>::type operator*() const;

pointer тип определяется как

std::remove_reference<Deleter>::type::pointer if that type exists, otherwise T*

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

#include <iostream>
#include <memory>

struct Foo { // object to manage
Foo() { std::cout << "Foo ctor\n"; }
Foo(const Foo&) { std::cout << "Foo copy ctor\n"; }
Foo(Foo&&) { std::cout << "Foo move ctor\n"; }
~Foo() { std::cout << "~Foo dtor\n"; }
};

struct Exception {};

struct InternalPtr {
Foo *ptr = nullptr;
InternalPtr(Foo *p) : ptr(p) {}
InternalPtr() = default;

Foo& operator*() const {
std::cout << "Checking for a null pointer.." << std::endl;
if(ptr == nullptr)
throw Exception();
return *ptr;
}

bool operator != (Foo *p) {
if(p != ptr)
return false;
else
return true;
}
void cleanup() {
if(ptr != nullptr)
delete ptr;
}
};

struct D { // deleter
using pointer = InternalPtr;
D() {};
D(const D&) { std::cout << "D copy ctor\n"; }
D(D&) { std::cout << "D non-const copy ctor\n";}
D(D&&) { std::cout << "D move ctor \n"; }
void operator()(InternalPtr& p) const {
std::cout << "D is deleting a Foo\n";
p.cleanup();
};
};

int main()
{
std::unique_ptr<Foo, D> up(nullptr, D()); // deleter is moved

try {
auto& e = *up;
} catch(Exception&) {
std::cout << "null pointer exception detected" << std::endl;
}

}

Живой пример

Для полноты картины я опубликую две дополнительные альтернативы / обходные пути:

  1. Проверка указателя на unique_ptr с помощью operator bool

    #include <iostream>
    #include <memory>
    
    int main()
    {
    std::unique_ptr<int> ptr(new int(42));
    
    if (ptr) std::cout << "before reset, ptr is: " << *ptr << '\n';
    ptr.reset();
    if (ptr) std::cout << "after reset, ptr is: " << *ptr << '\n';
    }
    

    (Это, вероятно, самый клановый способ решить проблему)

  2. Альтернативное решение, хотя и более сложное, должен использовать тип оболочки, который заботится об обработке исключений

17

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

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

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

Существует также школа, которая говорит, что вы не должны бросать исключения в ответ на ошибки программирования. Может быть, люди, отвечающие за дизайн unique_ptr принадлежал этой школе, в то время как люди, разрабатывающие вектор (который намного старше), этого не делали.

4

Одной из основных целей разработки API интеллектуальных указателей является замена с добавленной стоимостью, без дополнительных ошибок и побочных эффектов, а также с минимальными накладными расходами. if (ptr) ptr->... как обычно осуществляется безопасный доступ к голому указателю, тот же синтаксис прекрасно работает с умными указателями, поэтому не требуется никаких изменений кода при замене одного на другой.

Дополнительная проверка на достоверность (скажем, на выдачу исключения), помещенная в указатель, будет мешать предсказателю перехода и, таким образом, может оказывать влияние на производительность, что больше не может рассматриваться как замена с нулевой стоимостью.

3

У вас есть

operator bool()

Пример из:
cplusplusreference

// example of unique_ptr::operator bool
#include <iostream>
#include <memory>int main () {
std::unique_ptr<int> foo;
std::unique_ptr<int> bar (new int(12));

if (foo) std::cout << "foo points to " << *foo << '\n';
else std::cout << "foo is empty\n";

if (bar) std::cout << "bar points to " << *bar << '\n';
else std::cout << "bar is empty\n";

return 0;
}

unique_ptr — это простая оболочка для необработанного указателя, нет необходимости создавать исключение, когда вы можете просто проверить логическое условие.

Редактировать:
По-видимому оператор * могу бросить.

Исключения
1) может бросить, например, если указатель определяет оператор броска *

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

2

Следуя предложению MikeMB, здесь возможна реализация бесплатной функции для разыменования указателей и unique_ptrтак же.

template<typename T>
inline T& dereference(T* ptr) noexcept(false)
{
if(!ptr) throw std::runtime_error("attempt to dereference a nullptr");
return *ptr;
}

template<typename T>
inline T& dereference(std::unique_ptr<T> const& ptr) noexcept(false)
{
if(!ptr) throw std::runtime_error("attempt to dereference an empty unique_ptr)");
return *ptr;
}
2
По вопросам рекламы [email protected]