Частные члены против временных переменных в С++

Предположим, что у вас есть следующий код:

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 был инициализирован только один раз (из-за стоимости в этом случае). Затем я переместил его на одноэлементный шаблон.