Как избежать возврата дескрипторов к внутренним элементам объекта — статья 28. Эффективный C++

Пункт 28 Эффективного С++ говорит avoid returning "handles" to object internals. Этот вопрос показывает, как разработать код сделать именно это, думая об инкапсуляции, чтобы избежать случайного раскрытия внутренностей вашего класса.

В моем примере используется массив данных, и, поскольку проблема с памятью, я бы не хотел использовать std::vector (и библиотеку Boost).

Использование массива здесь очень упрощенная версия моего кода:

class Foo {
public:
    Foo(int size) : data_(new int[size]) {
        // Populate with data just for the example
        for(int i = 0; i < size; i++) {
            data_[i] = i;
        }
    }

    // I know that this isn't good practice
    int* const get_data() const {
        return data_;
    }

    ~Foo() {
        delete[] data_;
    }

private:
    int* data_;
};

int main(void) {
    Foo* foo = new Foo(10);
    int* const data = foo->get_data();
    delete foo;
    // data now dangles which is bad!
    return 0;
}

Я понимаю, что использование const с get_data() не делает его безопасным. Если бы я использовал вектор, я мог бы скопировать его, как в примере выше, но, поскольку я хотел бы избежать этого, мне было интересно, как лучше всего спроектировать мой класс, чтобы избежать этой потенциально опасной ситуации?


person Sarah Tattersall    schedule 15.04.2013    source источник
comment
std::vector имеет очень мало накладных расходов по сравнению с необработанным массивом и намного безопаснее и проще в использовании.   -  person Angew is no longer proud of SO    schedule 15.04.2013
comment
Если я не ошибаюсь, в Effective C++ также говорится, что вы должны использовать vector в любых случаях, за исключением того, что вы точно (!!!) уверены, что он будет действовать хуже, чем другой контейнер   -  person borisbn    schedule 15.04.2013


Ответы (5)


Программирование и дизайн программного обеспечения — это постоянный «выбор одного над другим». Если вы ДЕЙСТВИТЕЛЬНО не можете позволить себе использовать std::vector, и вам нужно предоставить данные клиентскому коду с помощью вашего класса, пусть будет так. Задокументируйте, почему это так, и убедитесь, что это пересматривается с течением времени — возможно, когда ваша новая модель оборудования с 8 МБ ОЗУ вместо 2 МБ сможет использовать дополнительные 4 байта, которые vector занимает ваш простой указатель .

Или напишите функцию доступа:

int& Foo::operator[](int pos)
{
   // Add checks here to ensure `pos` is in range?
   return data[pos]; 
}

Теперь вам вообще не нужно возвращать указатель...

person Mats Petersson    schedule 15.04.2013

Один из способов в вашей ситуации - использовать умные указатели. Я бы предпочел shared_ptr. Вот как вы можете изменить свой класс, чтобы избежать проблем, которые вы поднимаете:

class Foo {
public:
    Foo(int size) : data_(new int[size]) {
        // Populate with data just for the example
        for(int i = 0; i < size; i++) {
            data_[i] = i;
        }
    }

    // I know that this isn't good practice
    std::shared_ptr<int> const get_data() const {
        return data_;
    }

    ~Foo() {
        //delete[] data_;
    }

private:
    std::shared_ptr<int> data_;
};

int main(void) {
    Foo* foo = new Foo(10);
    std::shared_ptr<int> data = foo->get_data();
    delete foo;
    // data still good )) // --now dangles which is bad!--
    return 0;
}
person borisbn    schedule 15.04.2013
comment
Если она даже не может терпеть накладные расходы указателя в std::vector, интересно, как она может остаться с идеей накладных расходов shared_ptr и дублированием данных... - person Emilio Garavaglia; 15.04.2013
comment
@EmilioGaravaglia - хмммм... не думал об этом... но подумаю )) - person borisbn; 15.04.2013

Есть много случаев, когда возврат указателей вполне приемлем, даже если массив действительно содержится в std::vector; например использование внешних API и при копировании данных не приветствуется.

Я не вижу опасности, так как вы не программируете для обезьян. Пользователь класса должен знать. Но я добавил функцию для запроса размера данных. То есть для безопасности.

extern "C"{
void foobar( const int *data, unsigned int size );
}
class Foo {
public:
    Foo(unsigned int size) : size_(size), data_(new int[size]) {
        // Populate with data just for the example
        for(unsigned int i = 0; i < size; i++) {
            data_[i] = i;
        }
    }

    unsigned int get_size() const{return size_;}
    int* const get_data() const {
        return data_;
    }

    ~Foo() {
        delete[] data_;
    }

private:
    int* data_;
    unsigned int size_;
};

int main(void) {
    Foo* foo = new Foo(10);
    int* const data = foo->get_data();
    foobar( data, foo->get_size() );
    delete foo;
    return 0;
}
person Vaaksiainen    schedule 15.04.2013

учитывая, что даже в стандартной библиотеке есть get_native_handle() для таких классов, как std::thread, интересно, кто из стандартной библиотеки или EC++ был неправ!

Эффективный C++ — это набор руководящих принципов, которые вы просто не можете применить ни в каком контексте. Это "моральная" книга. Не относитесь к этому как к «законному». Если вам необходимо предоставить данные, просто сделайте это.

Ведь тот, кто удаляет указатель на ваши внутренние данные (после получения его через геттер), тоже делает «плохое дело», так как действует деструктивно на что-то ему не принадлежащее.

person Emilio Garavaglia    schedule 15.04.2013

Если вы действительно хотите избежать использования std::vector, вы можете реализовать подход, аналогичный тому, который используется некоторыми функциями winapi.

size_t get_data(int* data_ptr, size_t& buffer_size)
{
if(buffer_size < sizeof (data_)
{
buffer_size = sizeof(data);
return 0;
}
//You could you memcpy for ints here
        for(int i = 0; i < size; i++) {
            data_ptr[i] = i;
        }
return sizof(data_);
}

Таким образом, вызывающему объекту требуется предоставить память для хранения данных и заботиться о распределении этой памяти.
Таким образом, это должно быть переосмысление того, что std::vector уже делал бы с гораздо меньшей безопасностью. Также вам следует проверить Правило трех, чтобы избежать проблем, когда вы начнете использовать свой класс более широко.

person alexrider    schedule 15.04.2013