Сгруппируйте две функции, которые отличаются только 1 строкой кода
У меня есть две критически важные для производительности функции:
insertExpensive(Holder* holder, Element* element, int index){
//............ do some complex thing 1
holder->ensureRange(index);//a little expensive
//............ do some complex thing 2
}
insertCheap(Holder* holder, Element* element, int index){
//............ do some complex thing 1
//............ do some complex thing 2
}
Как сгруппировать 2 функции вместе, чтобы повысить ремонтопригодность?
Мои плохие решения:
Решение 1.
insertExpensive(Holder* holder, Element* element, int index){
do1();
holder->ensureRange(index);//a little expensive
do2();
}
insertCheap(Holder* holder, Element* element, int index){
do1();
do2();
}
Это было бы некрасиво.
Это также нецелесообразно, если do2
хочет получить некоторые локальные переменные из do1
.
Решение 2.
insert(Holder* holder, Element* element, int index, bool check){
//............ do some complex thing 1
if(check)holder->ensureRange(index);//a little expensive
//............ do some complex thing 2
}
Это стоит условной проверки для каждого вызова.
Решение 3. (проект)
template<bool check> insert(Holder* holder, Element* element, int index){
//............ do some complex thing 1 (Edit2 from do1());
bar<check>();
//............ do some complex thing 2 (Edit2 from do2());
}
template <>
inline void base_template<true>::bar() { holder->ensureRange(index); }
template <>
inline void base_template<false>::bar() { }
Перебор и ненужная сложность?
Правка 1:
Приоритет критериев того, насколько хорош подход, сортируются следующим образом: -
1. Лучшая производительность
2. Меньше дубликатов кода
3. Меньше общей строки кода
4. Легче читать для эксперта и новичка
Правка 2: правка 3-го решения. Спасибо мвидельгаузу и Вольфу.
5 ответов:
Ваше решение 2 на самом деле еще не так плохо. Если этот код находится в заголовке, он неявно рассматривается как встроенный код. (Я написал это явно) Если вы вызываете его с помощью true или false, компилятор может удалить оператор if, хотя это зависит от ряда факторов, чтобы знать, будет ли он это делать. (Размер всего тела после инлайнинга, видимость константы, настройка ...)
inline void insert(Holder* holder,Element* element,int index, bool check){ do1(); if (check) holder->ensureRange(index);//a little expensive do2(); }
Решение 3-это на самом деле то, чего вы хотите достичь, так как шаблоны требуют новой функции создание экземпляров для каждого отдельного вызова, чтобы удалить мертвый код. Однако он может быть написан очень похоже на то, как вы написали решение 2.
template <bool check> inline void insert(Holder* holder,Element* element,int index){ do1(); if (check) holder->ensureRange(index);//a little expensive do2(); }
Если у вас есть C++17, Вам больше не нужно зависеть от компилятора, чтобы удалить мертвый код, поскольку вы можете принудительно пропустить определенный код через constexpr-if. Эта конструкция гарантирует, что код в операторе if будет удален, так как он даже не должен компилироваться.
template <bool check> inline void insert(Holder* holder,Element* element,int index){ do1(); if constexpr (check) holder->ensureRange(index);//a little expensive do2(); }
insert(Holder* holder,Element* element,int index, bool ensureRange){ //............ do some complex thing 1 if (ensureRange){ holder->ensureRange(index);//a little expensive } //............ do some complex thing 2 }
И если вы можете принять решение во время компиляции и хотите использовать шаблоны:
template<bool check> insert(Holder* holder,Element* element,int index){ //............ do some complex thing 1; if(check)holder->ensureRange(index); //............ do some complex thing 2 } insert<true>(...); //expensive operation insert<false>(...); //cheap operation
Технически, я бы предпочел решение, как показано в ответеmvidelgauz , но добавленные аргументы выглядят плохо, когда функция должна быть фактически вызвана из-за не самоописывающих литералов
true
илиfalse
. Поэтому я бы объединил три функции: внутреннюю (private
) функцию, которая предоставляет флаг проверки в качестве аргумента, и две внешние функции, которые предоставляют очевидные имена функции, я бы предпочел что-то вродеcheckedInsert
,uncheckedInsert
. Эти публичные функции затем вызовут 4-аргумент реализация.В следующем фрагменте кода уменьшено большинство параметров, чтобы сделать решение наиболее очевидным. Его следует рассматривать как фрагмент определения класса:
public: /// performs a fast insert without range checking. Range-check yourself before! void checkedInsert(Element* element) { insertWithCheckOption(element, true); } /// performs a range-checked insert void uncheckedInsert(Element* element) { insertWithCheckOption(element, false); } private: /// implements insert with range check option void insertWithCheckOption(Element* element, bool doRangeCheck) { // ... do before code portion ... if (doRangeCheck) { // do expansive range checking ... } // ... do after code portion ... }
Это решение обеспечивает как наилучший пользовательский интерфейс, так и последовательную реализацию.
кстати: Я действительно сомневаюсь, что реальные проблемы с производительностью когда-либо будут вызваны одной условной проверкой.
Предположим, что вы создаете следующую структуру:
Класс, который определяет два защищенных метода
do1
иdo2
и открытый абстрактный методInsert
.Извините, если я допустил некоторые синтаксические ошибки, но именно так я вижу решение.class BaseHolder { proteted: void do1(/* complete with necessary parameters*/) { } void do2(/* complete with necessary parameters*/) { } public: abstract void Insert(Holder* holder, Element* element, int index); }; class Expensive : BaseHolder { public: void Insert(Holder* holder, Element* element, int index) { do1(); holder->ensureRange(index); do2(); } }; class Cheap : BaseHolder { public: void Insert(Holder* holder, Element* element, int index) { do1(); do2(); } };
Другая возможность состоит в том, чтобы сделать пользовательские
Cheap
иExpensive
классы, которые и обертываютHolder
, и в конструктореExpensive
делают проверку для диапазона:class Base { protected: Holder* _holder; public: Holder* GetHolder(){ return _holder; } } class Cheap : Base { public: Cheap(Holder* holder) { _holder = holder; } }; class Expensive : Base { public: Expensive(Holder* holder) { holder->ensureRange(index); _holder = holder; } };
Использование дешевых и дорогих объектов в качестве параметров к методу
Insert
. Я думаю, что второе решение лучше, чем первое.
И решение, более похожее на первое, но которое использует шаблон метода design pattern, так как лучшее приходит в конце:
class BaseHolder { proteted: void do1(/* complete with necessary parameters*/) { } void do2(/* complete with necessary parameters*/) { } virtual void check(Holder* holder, int index){ }; public: void Insert(Holder* holder, Element* element, int index) { do1(); check(holder, index); do2(); }; class Expensive : BaseHolder { protected: override void check(Holder* holder, int index) { holder->ensureRange(index); } }; class Cheap : BaseHolder { };
Это решение определяет метод
check
только на объектеExpensive
, оно имеет наибольшее повторное использование кода и определенно является одним из самых чистых подходов. К сожалению, речь идет о дизайне ООП больше, чем о производительности, где она не самая лучшая, но вам придется подумайте, что является вашим приоритетом, как вы только что заключили.
В качестве другой альтернативы можно просто использовать аргумент функции по умолчанию, значение по умолчанию которого соответствует отсутствию проверки диапазона
void insert(Holder* holder, Element* element, int index, bool performRangeCheck = false) { /* do some complex thing 1 */ if(performRangeCheck) { holder->ensureRange(index); } /* do some complex thing 2 */ } // ... insert(holder, element, 2); // no range check insert(holder, element, 2, true); // perform range check