Пункт 28 Эффективного C ++ говорит 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()
не делает это безопасным. Если бы я использовал вектор, я мог бы скопировать его, как в примере, приведенном выше, но, поскольку я хотел бы избежать этого, мне было интересно, как лучше спроектировать свой класс, чтобы избежать этой потенциально опасной ситуации?
Программирование и разработка программного обеспечения — это постоянный «выбор одной вещи над другой». Если вы действительно не можете позволить себе использовать std::vector
и вам нужно предоставить данные клиентскому коду, используя ваш класс, пусть будет так. Документируйте, почему это так, и удостоверьтесь, что это проверено с течением времени — возможно, когда ваша новая модель оборудования, имеющая 8 МБ ОЗУ вместо 2 МБ, сможет использовать дополнительные 4 байта, которые vector
берет на себя ваш простой указатель.
Или напишите функцию доступа:
int& Foo::operator[](int pos)
{
// Add checks here to ensure `pos` is in range?
return data[pos];
}
Теперь вам не нужно возвращать указатель вообще …
Единственный способ в вашей ситуации — использовать умные указатели. Я бы предпочел 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;
}
Существует множество случаев, когда возврат указателей является вполне приемлемым, даже если массив действительно содержался в 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;
}
учитывая, что даже стандартная библиотека имеет get_native_handle()
на занятиях вроде std::thread
Интересно, кто между стандартной библиотекой или EC ++ был не прав?
Эффективный C ++ — это набор рекомендаций, которые вы просто не можете применять в любом контексте. Это «моральная» книга. Не рассматривайте это как «законный». Если вам необходимо предоставить данные, просто сделайте это.
В конце концов, тот, кто удаляет указатель на ваши внутренние данные (после получения через геттер), также делает «плохую вещь», так как он действует разрушительно на что-то, не принадлежит ему.
Если вы действительно хотите избежать использования 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 уже будет делать с гораздо меньшей безопасностью.
Также вы должны проверить Правило трех чтобы избежать проблем, когда вы начнете использовать свой класс более широко.