Código duplicado usando c ++ 11

80

Actualmente estoy trabajando en un proyecto y tengo el siguiente problema.

Tengo un método C ++ que quiero trabajar de dos formas diferentes:

void MyFunction()
{
  foo();
  bar();
  foobar();
}

void MyFunctionWithABonus()
{
  foo();
  bar();
  doBonusStuff();
  foobar();
}

Y me gustaría no duplicar mi código porque la función real es mucho más larga. El problema es que no debo bajo ninguna circunstancia agregar tiempo de ejecución al programa cuando se llama a MyFunction en lugar de MyFunctionWithABonus. Es por eso que no puedo tener un parámetro booleano que verifico con una comparación de C ++.

Mi idea habría sido usar plantillas C ++ para duplicar virtualmente mi código, pero no puedo pensar en una forma de hacerlo en la que no tenga tiempo de ejecución adicional y no tenga que duplicar el código.

No soy un experto en plantillas, por lo que es posible que me falte algo.

¿Alguno de vosotros tiene una idea? ¿O es eso simplemente imposible en C ++ 11?

plougue
fuente
64
¿Puedo preguntar por qué no puede simplemente agregar un cheque booleano? Si hay mucho código allí, la sobrecarga de una simple verificación booleana será insignificante.
Joris
39
La predicción de @plougue Branch es muy buena hoy en día, hasta el punto de que una verificación booleana a menudo toma 0 ciclos de procesador para ejecutarse.
Dan
4
De acuerdo con @Dan. La predicción de sucursales tiene casi cero gastos generales en estos días, especialmente si ingresa a una sucursal en particular una gran cantidad de veces.
Akshay Arora
6
@Dan: Una comparación y bifurcación sigue siendo, en el mejor de los casos, una uop fusionada con macro (en las modernas CPU Intel y AMD x86 ), no cero. Dependiendo de cuál sea el cuello de botella en su código, decodificar / emitir / ejecutar este uop podría robar un ciclo de otra cosa, de la misma manera que podría hacerlo una instrucción ADD adicional. Además, simplemente pasar el parámetro booleano y hacer que bloquee un registro (o tenga que ser derramado / recargado) es un número de instrucciones distinto de cero. Con suerte, esta función está integrada para que la llamada y la sobrecarga de paso de arg no estén siempre presentes, y tal vez cmp + branch, pero aún así
Peter Cordes
15
¿Escribió primero el código en un formato fácil de mantener? Entonces, ¿su generador de perfiles dijo que la rama era el cuello de botella? ¿Tiene datos que sugieran que el tiempo que dedica a esta pequeña decisión es el mejor uso de su tiempo?
GManNickG

Respuestas:

55

Con template y lambda, puede hacer:

template <typename F>
void common(F f)
{
  foo();
  bar();
  f();
  foobar();
}

void MyFunction()
{
    common([](){});
}

void MyFunctionWithABonus()
{
  common(&doBonusStuff);
}

o simplemente puede crear prefixy suffixfuncionar.

void prefix()
{
  foo();
  bar();
}

void suffix()
{
    foobar();
}

void MyFunction()
{
    prefix();
    suffix();
}

void MyFunctionWithABonus()
{
    prefix();
    doBonusStuff();
    suffix();
}
Jarod42
fuente
12
De hecho, prefiero estas dos soluciones sobre un parámetro booleano (plantilla o de otro tipo), independientemente de las ventajas de tiempo de ejecución. No me gustan los parámetros booleanos.
Chris Drew
2
Según tengo entendido, la segunda solución tendrá un tiempo de ejecución adicional debido a la llamada de función adicional. ¿Es este el caso del primero? No estoy seguro de cómo funcionan las lambdas en ese caso
plougue
10
Si las definiciones están visibles, el compilador probablemente usaría código en línea y generaría el mismo código que el generado para su código original.
Jarod42
1
@Yakk Creo que dependerá del caso de uso particular y de quién es la responsabilidad de las "cosas extra". A menudo encuentro que tener parámetros bool, ifs y cosas extra en el algoritmo principal hace que sea más difícil de leer y preferiría que "ya no exista" y que se encapsule e inyecte desde otro lugar. Pero creo que la cuestión de cuándo es apropiado utilizar el Patrón de estrategia probablemente está más allá del alcance de esta pregunta.
Chris Drew
2
La optimización de llamadas de cola suele ser importante cuando desea optimizar los casos recursivos. En este caso, la alineación simple ... hace todo lo que necesita.
Yakk - Adam Nevraumont
128

Algo así funcionará bien:

template<bool bonus = false>
void MyFunction()
{
  foo();
  bar();
  if (bonus) { doBonusStuff(); }
  foobar();
}

Llámalo a través de:

MyFunction<true>();
MyFunction<false>();
MyFunction(); // Call myFunction with the false template by default

La plantilla "fea" se puede evitar agregando algunos envoltorios agradables a las funciones:

void MyFunctionAlone() { MyFunction<false>(); }
void MyFunctionBonus() { MyFunction<true>(); }

Allí puede encontrar buena información sobre esa técnica . Es un artículo "antiguo", pero la técnica en sí misma sigue siendo totalmente correcta.

Siempre que tenga acceso a un buen compilador de C ++ 17, incluso puede impulsar más la técnica, utilizando el constexpr si , así:

template <int bonus>
auto MyFunction() {
  foo();
  bar();
  if      constexpr (bonus == 0) { doBonusStuff1(); }
  else if constexpr (bonus == 1) { doBonusStuff2(); }
  else if constexpr (bonus == 2) { doBonusStuff3(); }
  else if constexpr (bonus == 3) { doBonusStuff4(); }
  // Guarantee that this function will not compile
  // if a bonus different than 0,1,2,3 is passer
  else { static_assert(false);}, 
  foorbar();
}
Gibet
fuente
11
Y esa verificación será optimizada muy bien por el compilador
Jonas
21
Y en C ++ 17 if constexpr (bonus) { doBonusStuff(); } .
Chris Drew
5
@ChrisDrew No estoy seguro de si el constexpr agregaría algo aquí. ¿Verdad?
Gibet
13
@Gibet: si la llamada a doBonusStuff()ni siquiera se puede compilar por alguna razón en el caso de no bonificación, hará una gran diferencia.
Lightness Races in Orbit
4
@WorldSEnder Sí, podría, si por enums o clase enum se refiere a constexpr (bonus == MyBonus :: ExtraSpeed).
Gibet
27

Dados algunos de los comentarios que el OP ha hecho con respecto a la depuración, aquí hay una versión que pide doBonusStuff()compilaciones de depuración, pero no compilaciones de lanzamiento (que definen NDEBUG):

#if defined(NDEBUG)
#define DEBUG(x)
#else
#define DEBUG(x) x
#endif

void MyFunctionWithABonus()
{
  foo();
  bar();
  DEBUG(doBonusStuff());
  foobar();
}

También puede usar la assertmacro si desea verificar una condición y fallar si es falsa (pero solo para compilaciones de depuración; las compilaciones de lanzamiento no realizarán la verificación).

Tenga cuidado si doBonusStuff()tiene efectos secundarios, ya que estos efectos secundarios no estarán presentes en las versiones de la versión y pueden invalidar las suposiciones hechas en el código.

Tallos de maiz
fuente
La advertencia sobre los efectos secundarios es buena, pero también es cierta sin importar qué construcción se use, ya sean plantillas, if () {...}, constexpr, etc.
pipe
Dados los comentarios del OP, yo mismo voté a favor de esto porque es exactamente la mejor solución para ellos. Dicho esto, solo una curiosidad: ¿por qué todas las complicaciones con las nuevas definiciones y todo, cuando puedes poner la llamada doBonusStuff () dentro de un #if definido (NDEBUG)?
motoDrizzt
@motoDrizzt: Si el OP quiere hacer lo mismo en otras funciones, encuentro la introducción de una nueva macro como esta más limpia / más fácil de leer (y escribir). Si es algo que se hace una sola vez, entonces estoy de acuerdo en que usarlo #if defined(NDEBUG)directamente es probablemente más fácil.
Cornstalks
@Cornstalks sí, tiene mucho sentido, no lo pensé tan lejos. Y sigo pensando que esta debería ser la respuesta aceptada :-)
motoDrizzt
18

Aquí hay una ligera variación en la respuesta de Jarod42 usando plantillas variadas para que la persona que llama pueda proporcionar funciones de bonificación cero o una:

void callBonus() {}

template<typename F>
void callBonus(F&& f) { f(); }

template <typename ...F>
void MyFunction(F&&... f)
{
  foo();
  bar();
  callBonus(std::forward<F>(f)...);
  foobar();
}

Código de llamada:

MyFunction();
MyFunction(&doBonusStuff);
Chris Drew
fuente
11

Otra versión, usando solo plantillas y sin funciones de redireccionamiento, ya que dijiste que no querías ninguna sobrecarga de tiempo de ejecución. Por lo que a mí respecta, esto solo aumenta el tiempo de compilación:

#include <iostream>

using namespace std;

void foo() { cout << "foo\n"; };
void bar() { cout << "bar\n"; };
void bak() { cout << "bak\n"; };

template <bool = false>
void bonus() {};

template <>
void bonus<true>()
{
    cout << "Doing bonus\n";
};

template <bool withBonus = false>
void MyFunc()
{
    foo();
    bar();
    bonus<withBonus>();
    bak();
}

int main(int argc, const char* argv[])
{
    MyFunc();
    cout << "\n";
    MyFunc<true>();
}

output:
foo
bar
bak

foo
bar
Doing bonus
bak

Ahora solo hay una versión de MyFunc()con el boolparámetro como argumento de plantilla.

Sebastián Stern
fuente
¿No agrega tiempo de compilación llamando a bonus ()? ¿O el compilador detecta que el bono <false> está vacío y no ejecuta la llamada a la función?
Plougue
1
bonus<false>()invoca la versión predeterminada de la bonusplantilla (líneas 9 y 10 del ejemplo), por lo que no hay una llamada a la función. Para decirlo de otra manera, MyFunc()compila en un bloque de código (sin condicionales en él) y MyFunc<true>()compila en un bloque de código diferente (sin condicionales en él).
David K
6
Las plantillas de @plougue están implícitamente en línea, y las funciones vacías en línea no hacen nada y el compilador puede eliminarlas.
Yakk - Adam Nevraumont
8

Puede utilizar el envío de etiquetas y la sobrecarga de funciones simples:

struct Tag_EnableBonus {};
struct Tag_DisableBonus {};

void doBonusStuff(Tag_DisableBonus) {}

void doBonusStuff(Tag_EnableBonus)
{
    //Do bonus stuff here
}

template<class Tag> MyFunction(Tag bonus_tag)
{
   foo();
   bar();
   doBonusStuff(bonus_tag);
   foobar();
}

Esto es fácil de leer / entender, se puede expandir sin problemas (y sin ifcláusulas repetitivas , agregando más etiquetas) y, por supuesto, no dejará huella en el tiempo de ejecución.

La sintaxis de llamada es bastante amigable, pero, por supuesto, se puede envolver en llamadas vanilla:

void MyFunctionAlone() { MyFunction(Tag_DisableBonus{}); }
void MyFunctionBonus() { MyFunction(Tag_EnableBonus{}); }

El envío de etiquetas es una técnica de programación genérica ampliamente utilizada, aquí hay una buena publicación sobre los conceptos básicos.

Ap31
fuente