Сгруппируйте две функции, которые отличаются только 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 4

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