Я задумываюсь над тем, хочу ли я писать, как Код 1 против Кодекса 2. На мой взгляд, Код 1 выглядит чище, но теоретически можно ли ожидать снижения производительности из-за его дополнительных косвенных ссылок по сравнению с Кодом 2? Есть ли здесь какие-либо подходящие оптимизации компилятора? Что-то меняется, если bar () возвращает Bar *?
Код 1:
foo.bar().method1();
foo.bar().method2();
foo.bar().method3();
foo.bar().method4();
Код 2:
Bar& bar = foo.bar(); //Java programmers: ignore ampersand
bar.method1();
bar.method2();
bar.method3();
bar.method4();
РЕДАКТИРОВАТЬ:
Я думаю, что слишком много переменных, чтобы задать такой общий вопрос (например, const против неконстантных методов, указывает ли компилятор методы на методы, как компилятор обрабатывает ссылки и т. Д.). Анализ моего конкретного кода на ассемблере — это, пожалуй, путь.
Code_1, кажется, имеет снижение производительности по сравнению с Code_2.
Но помните самое основное правило надежных конструкций C ++: Преждевременная оптимизация — корень всего зла. Сначала создайте свой код для ясности, а затем назначьте хорошего профилировщика своим «Гуру».
Второй вариант, Bar bar = foo.bar()
определенно более эффективный, хотя, насколько это зависит, зависит от веса штанги. Разница вполне может быть тривиальной; попробуйте бенчмаркинг.
Что касается читабельности, я бы сказал, что второй вариант более читабелен, но это становится личным стилем. Я думаю, что вы действительно хотите, хотя это method5
который вызывает все четыре метода внутри. Таким образом, вы можете иметь
foo.bar().method5();
И это все.
В зависимости от того, что bar
на самом деле, может быть или не может быть (заметно) снижение производительности.
Более интересным вопросом является то, что заставляет вас думать, что ваш первый подход «чище».
Не зная каких-либо подробностей реализации, я на самом деле склонен думать об обратном: последний подход не только короче (короткий — это хорошо, потому что меньше кода — меньше ошибок и меньше материала для чтения), но также чище и более читабелен.
Он четко отражает намерения автора и не оставляет читателя задуматься о специфике реализации bar
и это может привести к непредвиденным побочным эффектам, которые, в свою очередь, могут быть или не быть преднамеренными и / или желательными.
Не делайте этого, если у вас нет очень веских причин.
Контрольные тесты
Я провел простой тест. Когда скомпилировано с без оптимизации, на моей машине Test_1 заняло 1272 мс, а Test_2 — 1108 (тесты я запускал несколько раз, результаты в течение пары мс). С оптимизацией O2 / O3, оба теста заняли одинаковое количество времени: 946 мс.
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <chrono>
using namespace std;
class Foo
{
public:
Foo() : x_(0) {}
void add(unsigned amt)
{
x_ += amt;
}
unsigned x_;
};
class Bar
{
public:
Foo& get()
{
return foo_;
}
private:
Foo foo_;
};
int main()
{
srand(time(NULL));
Bar bar;
constexpr int N = 100000000;
//Foo& foo = bar.get(); //TEST_2
auto start_time = chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i)
{
bar.get().add(rand()); //TEST_1
//foo.add(rand()); //TEST_2
}
auto end_time = chrono::high_resolution_clock::now();
cout << bar.get().x_ << endl;
cout << "Time: ";
cout << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << endl;
}
Тесты указателя
Я перезапустил тесты, но на этот раз с указанием члена класса. Когда скомпилировано с без оптимизации, на моей машине Тест_3 занял 1285-1340 мс, а Тест_4 — 1110 мс. С оптимизацией O2 / O3, оба теста, по-видимому, занимают одинаковое количество времени: 915 мс (удивительно, меньше времени, чем приведенные выше контрольные тесты).
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <chrono>
using namespace std;
class Foo
{
public:
Foo() : x_(0) {}
void add(unsigned amt)
{
x_ += amt;
}
unsigned x_;
};
class Bar
{
public:
~Bar()
{
delete foo_;
}
Foo* get()
{
return foo_;
}
private:
Foo* foo_ = new Foo;
};
int main()
{
srand(time(NULL));
Bar bar;
constexpr int N = 100000000;
//Foo* foo = bar.get(); //TEST_4
auto start_time = chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i)
{
bar.get()->add(rand()); //TEST_3
//foo->add(rand()); //TEST_4
}
auto end_time = chrono::high_resolution_clock::now();
cout << bar.get()->x_ << endl;
cout << "C++ Time: ";
cout << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << endl;
}
Заключение
Согласно этим простым тестам на моей машине, стиль кода 2 немного быстрее, примерно на ~ 15%, когда оптимизации не включен, но с включенными оптимизациями различий в производительности нет.
Я бы сказал, что в большинстве случаев ни один из двух вариантов не является хорошим выбором. Оба раскрывают внутренние данные посредством ссылки, которая нарушает инкапсуляцию. Я думаю, что уровень абстракции объекта Foo слишком низок для его использования, и он должен предлагать больше сервис-подобных функций, чтобы что-то с ним делать.
Вместо
Bar& bar = foo.bar(); //Java programmers: ignore ampersand
bar.method1();
bar.method2();
bar.method3();
bar.method4();
Там должен быть какой-то метод более высокого уровня в Foo
class Foo
{
public:
void doSomething()
{
Bar& bar = foo.bar(); //Java programmers: ignore ampersand
bar.method1();
bar.method2();
bar.method3();
bar.method4();
}
private:
Bar& bar();
};
Вы никогда не должны давать неконстантные ссылки на внутренние компоненты клиентам.