Частные члены против временных переменных в С++
Предположим, что у вас есть следующий код:
int main(int argc, char** argv) {
Foo f;
while (true) {
f.doSomething();
}
}
Какая из следующих двух реализаций Foo предпочтительнее?
Решение 1:
class Foo {
private:
void doIt(Bar& data);
public:
void doSomething() {
Bar _data;
doIt(_data);
}
};
Решение 2:
class Foo {
private:
Bar _data;
void doIt(Bar& data);
public:
void doSomething() {
doIt(_data);
}
};
В простом английском: если у меня есть класс с методом, который вызывается очень часто, и этот метод определяет значительное количество временных данных (либо один объект сложного класса, либо большое количество простых объектов), должен Я объявляю эти данные как частные члены класса?
С одной стороны, это сэкономит время, затрачиваемое на создание, инициализацию и разрушение данных для каждого вызова, что повысит производительность. С другой стороны, он попирает принцип "частный член = состояние объекта" и может затруднить понимание кода.
Ответ зависит от размера/сложности класса Bar? Как насчет количества объявленных объектов? В какой момент выгоды перевесят недостатки?
Ответы
Ответ 1
С точки зрения дизайна использование временных ресурсов является более чистым, если эти данные не являются частью состояния объекта и должны быть предпочтительными.
Никогда не делайте выбор дизайна на основе производительности перед фактическим профилированием приложения. Вы можете просто обнаружить, что в итоге вы получаете худший дизайн, который на самом деле не лучше, чем исходная проектная производительность.
Во всех ответах, рекомендующих повторно использовать объекты, если затраты на строительство/уничтожение высоки, важно отметить, что если вы должны повторно использовать объект из одного вызова в другой, во многих случаях объект должен быть reset действительное состояние между вызовами метода, а также стоимость. Во многих таких случаях стоимость сброса может быть сопоставима со строительством/разрушением.
Если вы не reset состояние объекта между вызовами, два решения могут давать разные результаты, как в первом вызове, аргумент будет инициализирован, и состояние, вероятно, будет отличаться между вызовами метода.
Безопасность потоков также оказывает большое влияние на это решение. Автоматические переменные внутри функции создаются в стеке каждого из потоков и как таковые по сути являются потокобезопасными. Любая оптимизация, которая толкает эту локальную переменную, чтобы ее можно было повторно использовать между различными вызовами, будет затруднять безопасность потоков и даже может привести к снижению производительности из-за конкуренции, которая может ухудшить общую производительность.
Наконец, если вы хотите сохранить объект между вызовами метода, я все равно не сделаю его частным членом класса (он не является частью класса), а скорее деталью реализации (статическая функциональная переменная, глобальная в неназванной пространство имен в блоке компиляции, где реализована doOperation, член PIMPL... [первые 2 обмена данными для всех объектов, а последние только для всех вызовов в одном и том же объекте]) пользователям вашего класса не важно, как вы решаете вещи (пока вы делаете это безопасно и документируете, что класс не является потокобезопасным).
// foo.h
class Foo {
public:
void doOperation();
private:
void doIt( Bar& data );
};
// foo.cpp
void Foo::doOperation()
{
static Bar reusable_data;
doIt( reusable_data );
}
// else foo.cpp
namespace {
Bar reusable_global_data;
}
void Foo::doOperation()
{
doIt( reusable_global_data );
}
// pimpl foo.h
class Foo {
public:
void doOperation();
private:
class impl_t;
boost::scoped_ptr<impl_t> impl;
};
// foo.cpp
class Foo::impl_t {
private:
Bar reusable;
public:
void doIt(); // uses this->reusable instead of argument
};
void Foo::doOperation() {
impl->doIt();
}
Ответ 2
Прежде всего, это зависит от решаемой проблемы. Если вам нужно сохранить значения временных объектов между вызовами, вам нужна переменная-член. Если вам нужно повторно инициализировать их при каждом вызове, используйте локальные временные переменные. Это вопрос поставленной задачи, а не правильный или неправильный.
Создание и уничтожение временных переменных потребует дополнительного времени (по сравнению с просто сохраняющейся переменной-членом) в зависимости от того, насколько сложны классы временных переменных и что должны делать их конструкторы и деструкторы. Решение о значимости стоимости должно быть сделано только после профилирования, не пытайтесь оптимизировать его "на всякий случай".
Ответ 3
Я бы объявлял _data как временную переменную в большинстве случаев. Единственный недостаток - производительность, но вы получите больше преимуществ. Вы можете попробовать шаблон прототипа, если создание и разрушение действительно являются убийцами производительности.
Ответ 4
Если семантически корректно сохранять значение Bar внутри Foo, то нет ничего плохого в том, чтобы сделать его членом - тогда каждый Foo имеет-a.
Существует несколько сценариев, где это может быть неверно, например
- Если у вас есть несколько потоков, выполняющих doSomething, им нужны все отдельные экземпляры Bar или они могут принять один?
- было бы плохо, если бы состояние из одного вычисления переносилось на следующее вычисление.
В большинстве случаев проблема 2 - причина создания локальных переменных: вы должны быть уверены, чтобы начать с чистого состояния.
Ответ 5
Как и многие ответы на кодирование, это зависит.
Решение 1 намного более безопасно для потоков. Так что если doSomething вызывается многими потоками, я бы пошел на Решение 1.
Если вы работаете в одном потоковом окружении, и стоимость создания объекта Bar высока, я бы выбрал решение 2.
В одном потоковом env, и если стоимость создания бара низкая, тогда я думаю, что пойду для решения 1.
Ответ 6
Вы уже рассмотрели принцип "частный член = состояние объекта", поэтому нет смысла повторять это, однако, взгляните на него по-другому.
Множество методов, скажем, a, b и c, берут данные "d" и работают над ним снова и снова. Никакие другие методы класса не заботятся об этих данных. В этом случае вы уверены, что a, b и c находятся в правильном классе?
Было бы лучше создать еще один меньший класс и делегат, где d может быть переменной-членом? Такие абстракции трудно придумать, но часто приводят к большому коду.
Только мои 2 цента.
Ответ 7
Является ли это чрезвычайно упрощенным примером? Если нет, что не так с этим делать
void doSomething(Bar data);
int main() {
while (true) {
doSomething();
}
}
путь? Если doSomething()
- это чистый алгоритм, которому нужны некоторые данные (Bar
) для работы, зачем вам нужно обернуть его в класс? Класс предназначен для переноса состояния (данных) и путей (функций-членов) для его изменения.
Если вам просто нужна часть данных, используйте именно это: кусок данных. Если вам нужен только алгоритм, используйте функцию. Только если вам нужно сохранить состояние (значения данных) между вызовами нескольких алгоритмов (функций), работающих над ними, класс может быть правильным выбором.
Я признаю, что границы между ними размыты, но IME они делают хорошее эмпирическое правило.
Ответ 8
Если это действительно то временное, что стоит вам времени, тогда я бы сказал, что нет ничего плохого в том, чтобы включить его в свой класс в качестве участника. Но учтите, что это, возможно, сделает вашу функцию небезопасной, если она используется без правильной синхронизации - еще раз это зависит от использования _data
.
Я бы, однако, пометил такую переменную как mutable
. Если вы прочитали определение класса с членом mutable
, вы можете сразу предположить, что он не учитывает значение его родительского объекта.
class Foo {
private:
mutable Bar _data;
private:
void doIt(Bar& data);
public:
void doSomething() {
doIt(_data);
}
};
Это также позволит использовать _data
как изменяемый объект внутри функции const - так же, как вы могли бы использовать его как изменяемый объект, если бы это была локальная переменная внутри такой функции.
Ответ 9
Если вы хотите, чтобы Bar был инициализирован только один раз (из-за стоимости в этом случае). Затем я переместил его на одноэлементный шаблон.