Правильно ли я использую класс указателя в этом универсальном удалителе unique_ptr <> ()? - PullRequest
0 голосов
/ 25 июня 2018

Я создал универсальный шаблон удаления, который можно использовать для создания unique_ptr<>() подтипов, допускающих Deleter, отличных от delete ptr.

Отлично работает с флагами оптимизации по умолчанию (т.е. -O0), однако, когда я использую -O3, функция T & operator * () почему-то возвращает 0 вместо содержимого f_pointer.

Я хотел бы убедиться, что мы согласны с тем, что в компиляторе что-то не так и мой шаблон верен. Ниже приведен полный фрагмент кода, который должен быть скомпилирован в соответствии с Ubuntu 16.04 и Ubuntu 18.04 и, возможно, другими версиями, если они поддерживают C ++ 14 (см. Ниже для протестированных версий g ++).

// RAII Generic Deleter -- allow for any type of RAII deleter
//
// To break compile with:
//     g++ --std=c++14 -O3 -DNDEBUG ~/tmp/b.cpp -o b

#include <memory>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

template<class T, T null_value, class D, D deleter>
class raii_generic_deleter
{
public:
    class pointer
    {
    private:
        T f_pointer = null_value;

    public:
        pointer(T p)
            : f_pointer(p)
        {
        }

        pointer(std::nullptr_t = nullptr)
            : f_pointer(null_value)
        {
        }

        explicit operator bool () const
        {
            return f_pointer != null_value;
        }

        bool operator == (pointer const rhs) const
        {
            return f_pointer == rhs.f_pointer;
        }

        bool operator != (pointer const rhs) const
        {
            return f_pointer != rhs.f_pointer;
        }

        T & operator * ()
        {
            return f_pointer;
        }
    };

    void operator () (pointer p)
    {
        deleter(*p);
    }
};


typedef std::unique_ptr<int,
            raii_generic_deleter<int, -1, decltype(&::close), &::close>>
                        raii_fd_t;


int main(int argc, char * argv [])
{
    int fd = -1;

    {
        raii_fd_t safe_fd;

        std::cout << "default initialization: safe_fd = " << *safe_fd
                  << std::endl;

        fd = open("/tmp/abc.tmp", O_RDWR | O_CREAT, 0700);

        std::cout << "fd = " << fd << std::endl;

        safe_fd.reset(fd);

        std::cout << "safe_fd after the reset(" << fd
                  << ") = " << *safe_fd << std::endl;
    }

    if(fd != -1)
    {
        // assuming the safe_fd worked as expected, this call returns an error
        //
        int r = close(fd);
        int e(errno);

        std::cout << "second close returned " << r
                  << " (errno = " << e << ")" << std::endl;
    }

    return 0;
}

(Оригинал см. raii_generic_deleter.h на веб-сайтах libsnapwebs)

Есть вывод, который я получаю, когда использую -O0 (без оптимизации):

default initialization: safe_fd = -1
fd = 3
safe_fd after the reset(3) = 3
second close returned -1 (errno = 9)

В этом случае вызов *safe_fd возвращает -1 и 3, как и ожидалось. Это вызывает функцию шаблона T & pointer::operator * ().

При любом уровне оптимизации (-O1, -O2, -O3) вывод выглядит следующим образом:

default initialization: safe_fd = 0
fd = 3
safe_fd after the reset(3) = 0
second close returned -1 (errno = 9)

Как мы видим, дескриптор безопасного файла возвращает 0 вместо -1 после инициализации, а затем снова 0, когда оно должно быть 3. Однако деструктор правильно закрывает файл, так как второе закрытие завершается неудачно, как и ожидалось. Другими словами, каким-то образом описание файла (3) известно и правильно используется удалителем.

Когда я обновляю оператор указателя следующим образом:

        T & operator * ()
        {
            std::cout << "f_pointer within operator * = " << f_pointer
                      << std::endl;
            return f_pointer;
        }

Тогда вывод с любым уровнем оптимизации будет правильным:

f_pointer within operator * = -1
default initialization: safe_fd = -1
fd = 3
f_pointer within operator * = 3
safe_fd after the reset(3) = 3
f_pointer within operator * = 3
second close returned -1 (errno = 9)

Вероятно, это связано с тем, что эта конкретная функция не оптимизирована полностью.

Составители:

Я тестировал со стоковым g ++ на Ubuntu 16.04

g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.9) 5.4.0 20160609

А также на Ubuntu 18.04

g ++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0

Я также сообщил об этом как об ошибке на веб-сайте GNU .

Ответы [ 2 ]

0 голосов
/ 26 июня 2018

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

Как упомянуто @nm в принятом ответе, int не является указателем и, следовательно, к нему нельзя получить доступ как к указателю.Так что T & operator * () не логично для такого типа (поскольку operator [] () и operator -> () также не имеют смысла).

Проблема с кодом связана с тем, что get() используетсяoperator * () в реализации unique_ptr<>().Это выглядит так:

/// Return the stored pointer.
pointer
get() const noexcept
{ return std::get<0>(_M_t); }

Как мы видим, get() возвращает копию pointer.Такой объект временный!Это означает, что вы не можете вернуть ссылку на нее и надеетесь, что она останется действительной.operator * () делает это, хотя:

/// Dereference the stored pointer.
typename add_lvalue_reference<element_type>::type
operator*() const
{
  _GLIBCXX_DEBUG_ASSERT(get() != pointer());
  return *get();
}

Как мы видим, эта функция возвращает содержимое ссылки, возвращенной get(), но затем говорит, что хочет ссылку.Вот почему моя operator * () реализация должна была возвращать неконстантную ссылку.

Удаляя это ограничение и используя operator T () const вместо operator * () для получения значения unique_ptr<>(), я получаюправильное значение.

// RAII Generic Deleter -- allow for any type of RAII deleter
//
// To break compile with:
//     g++ --std=c++14 -O3 -DNDEBUG ~/tmp/b.cpp -o b

#include <memory>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

template<class T, T null_value, class D, D deleter>
class raii_generic_deleter
{
public:
    class pointer
    {
    private:
        T f_pointer = null_value;

    public:
        pointer(T p)
            : f_pointer(p)
        {
        }

        pointer(std::nullptr_t = nullptr)
            : f_pointer(null_value)
        {
        }

        explicit operator bool () const
        {
            return f_pointer != null_value;
        }

        bool operator == (pointer const rhs) const
        {
            return f_pointer == rhs.f_pointer;
        }

        bool operator != (pointer const rhs) const
        {
            return f_pointer != rhs.f_pointer;
        }

        operator T () const
        {
            return f_pointer;
        }
    };

    void operator () (pointer p)
    {
        deleter(static_cast<T>(p));
    }
};


typedef std::unique_ptr<int,
            raii_generic_deleter<int, -1, decltype(&::close), &::close>>
                        raii_fd_t;


int main(int argc, char * argv [])
{
    int fd = -1;

    {
        raii_fd_t safe_fd;

        std::cout << "default initialization: safe_fd = " << safe_fd.get()
                  << std::endl;

        fd = open("/tmp/abc.tmp", O_RDWR | O_CREAT, 0700);

        std::cout << "fd = " << fd << std::endl;

        safe_fd.reset(fd);

        std::cout << "safe_fd after the reset(" << fd
                  << ") = " << safe_fd.get() << std::endl;
    }

    if(fd != -1)
    {
        // assuming the safe_fd worked as expected, this call returns an error
        //
        int r = close(fd);
        int e(errno);

        std::cout << "second close returned " << r
                  << " (errno = " << e << ")" << std::endl;
    }

    return 0;
}

Таким образом, два основных изменения (1) T & operator * () стали operator T () const и (2) в main(), изменение *safe_fd в safe_fd.get().

Примечание: я также приведу значение f_pointer к T для функции удаления, если функция удаления не совпадает один к одному и произойдет сбой автоматического приведения.(т.е. в моем случае здесь close(int) имеет точный T в качестве ввода, так что он будет работать просто отлично. Иногда удалитель может быть не настолько идеальным.)

0 голосов
/ 25 июня 2018

Кажется, проблема связана с реализацией unique_ptr::operator* в libstdc ++.Здесь это очень упрощенным, урезанным способом:

struct pointer
{
    pointer(int val = -42) : z(val) { }
    int z = -42;
    int& operator*() { return z; }
};

struct my_unique_ptr
{
    pointer rep;
    pointer get() { return rep; }
#ifdef PROBLEM
    int& operator*() { return *get(); } // libstdc++ implementation
#else
    int& operator*() { return *rep; } // libc++ implementation
#endif
};

int main()
{
    my_unique_ptr q;
    std::cout << *q << "\n";
}

Теперь совершенно очевидно, что libstdc ++ не может работать с вашей реализацией pointer, потому что она возвращает ссылку на локальный временный объектот operator*.Любая pointer, которая хранит свой собственный pointee, будет иметь ту же проблему.

Стандартно, это не похоже на ошибку в libstdc ++.Стандарт определяет, что unique_ptr::operator*() возвращает *get(), что верно выполняет libstdc ++.

В любом случае, это дефект в стандарте.

Немедленное исправление - прекратить определять operator*в вашем pointer классе.unique_ptr это не нужно (NullablePointer не требуется для его предоставления).

Поскольку pointer на самом деле является не более чем оболочкой вокруг T, которая обеспечивает инициализацию значения для данной константы, было бы более разумно определить для нее operator T() и использовать get() для "разыменования" соответствующего unique_ptr.

...