Código duplicado usando c ++ 11

10 minutos de lectura

avatar de usuario
plogueo

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

Tengo un método C++ que quiero trabajar de dos maneras 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 bajo ninguna circunstancia debo agregar tiempo de ejecución al programa cuando se llama MyFunction en lugar de MyFunctionWithABonus. Es por eso que no puedo simplemente tener un parámetro booleano que verifique con una comparación de C++.

Mi idea habría sido usar plantillas de 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, así que puede que me esté perdiendo algo.

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

  • Puedo preguntar por qué ¿No puede simplemente agregar un control booleano? Si hay mucho código allí, la sobrecarga de una simple verificación booleana será insignificante.

    – Joris

    25 de abril de 2017 a las 8:47


  • La predicción de @plougue Branch es muy buena hoy en día, hasta el punto de que una verificación booleana a menudo requiere 0 ciclos de procesador para ejecutarse.

    – Dan

    25 de abril de 2017 a las 11:24

  • De acuerdo con @Dan. Tomas de predicción de bifurcación casi cero gastos generales en estos días, especialmente si ingresa a una sucursal en particular una gran cantidad de veces.

    – Akshay Arora

    25 de abril de 2017 a las 14:13

  • @Dan: una comparación y ramificación sigue siendo, en el mejor de los casos, una uop fusionada con macros (en las CPU Intel y AMD x86 modernas), 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 ate un registro (o que se derrame/recargue) es un número de instrucciones distinto de cero. Con suerte, esta función se alinea para que la sobrecarga de llamada y paso de argumentos no esté allí siempre, y tal vez cmp + rama, pero aún así

    – Peter Cordes

    25 de abril de 2017 a las 17:13

  • ¿Escribió primero el código en el formato fácil de mantener? Entonces, ¿su perfilador dijo que la sucursal era el cuello de botella? ¿Tiene datos que sugieran que el tiempo que dedica a esta decisión menor es el mejor uso de su tiempo?

    – GManNickG

    25 de abril de 2017 a las 22:36

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>(); }

Puedes encontrar buena información sobre esa técnica. allá. Ese es un papel “viejo”, pero la técnica en sí misma se mantiene totalmente correcta.

Siempre que tenga acceso a un buen compilador de C++ 17, incluso puede impulsar la técnica, utilizando el constexpr sicomo eso:

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();
}

  • Y esa verificación será muy bien optimizada por el compilador.

    – Jonás

    25 de abril de 2017 a las 8:40

  • Y en C++17 if constexpr (bonus) { doBonusStuff(); }.

    – Chris Drew

    25 de abril de 2017 a las 8:41


  • @Gibet: Si la llamada a doBonusStuff() ni siquiera puede compilar por alguna razón en el caso de no bonificación, hará una gran diferencia.

    – Carreras de ligereza en órbita

    25 de abril de 2017 a las 9:44

  • @Gibet Para su ejemplo de C++ 17, sería mejor si al final del if constexpr cadena, agregas un else { static_assert(false);}. De esta manera, no se puede crear una plantilla con valores no deseados.

    – KevinZ

    26 de abril de 2017 a las 15:49

  • Creo que la primera parte de la respuesta podría agregar definiciones de funciones en línea dando nombres separados a MyFunction<true> y para MyFunction<false>de modo que la persona que llama se libera de la necesidad de proporcionar argumentos de plantilla explícitos y, de hecho, puede olvidarse de que está llamando a una plantilla de función.

    –Marc van Leeuwen

    27 de abril de 2017 a las 9:24

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 puedes crear prefix y suffix función.

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

void suffix()
{
    foobar();
}

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

void MyFunctionWithABonus()
{
    prefix();
    doBonusStuff();
    suffix();
}

  • De hecho, prefiero estas dos soluciones a un parámetro booleano (plantilla o de otro tipo), independientemente de las ventajas del tiempo de ejecución. No me gustan los parámetros booleanos.

    – Chris Drew

    25 de abril de 2017 a las 8:56

  • Según tengo entendido, la segunda solución tendrá 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

    25 de abril de 2017 a las 9:54

  • Si las definiciones son visibles, el compilador probablemente generaría un código en línea y generaría el mismo código que el generado para su código original.

    – Jarod42

    25 de abril de 2017 a las 10:17

  • @Yakk Creo que dependerá del caso de uso particular y de quién es la responsabilidad de las “cosas adicionales”. A menudo encuentro que tener parámetros bool, ifs y cosas adicionales entre 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 pregunta de cuándo es apropiado usar el Patrón de estrategia probablemente esté más allá del alcance de esta pregunta.

    – Chris Drew

    25 de abril de 2017 a las 14:26

  • La optimización de llamadas de cola suele ser importante cuando desea optimizar casos recursivos. En este caso, simple en línea… hace todo lo que necesita.

    – Yakk – Adam Nevraumont

    26 de abril de 2017 a las 2:15

Teniendo en cuenta algunos de los comentarios que ha hecho el OP con respecto a la depuración, aquí hay una versión que llama doBonusStuff() para 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 utilizar el assert macro 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).

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

  • 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.

    – tubería

    27 de abril de 2017 a las 0:43

  • Dados los comentarios de OP, yo mismo voté 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 eso, cuando puedes simplemente poner la llamada doBonusStuff() dentro de un #ifdefined(NDEBUG) ??

    – moto Drizzt

    27 de abril de 2017 a las 8:52

  • @motoDrizzt: Si el OP quiere hacer lo mismo en otras funciones, encuentro que la introducción de una nueva macro como esta es más limpia/más fácil de leer (y escribir). Si es solo una cosa de una sola vez, entonces estoy de acuerdo solo con usar #if defined(NDEBUG) directamente es probablemente más fácil.

    – Tallos de maiz

    27 de abril de 2017 a las 11:45

  • @Cornstalks sí, tiene mucho sentido, no lo pensé de esa manera. Y sigo pensando que esta debería ser la respuesta aceptada 🙂

    – moto Drizzt

    27/04/2017 a las 11:50

avatar de usuario
chris dibujó

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

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);

Otra versión, usando solo plantillas y sin funciones de redirección, ya que dijiste que no querías ninguna sobrecarga de tiempo de ejecución. En 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 bool parámetro como un argumento de plantilla.

  • ¿No agrega tiempo de compilación llamando a bonus() ? ¿O el compilador detecta que bonus está vacío y no ejecuta la llamada de función?

    – plougue

    25 de abril de 2017 a las 9:50

  • bonus<false>() invoca la versión predeterminada del bonus plantilla (líneas 9 y 10 del ejemplo), por lo que no hay llamada de función. Para decirlo de otra manera, MyFunc() compila en un bloque de código (sin condicionales) y MyFunc<true>() compila en un bloque de código diferente (sin condicionales).

    – David K.

    25 de abril de 2017 a las 13:01

  • 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

    25 de abril de 2017 a las 13:56

avatar de usuario
ap31

Puedes usar envío de etiquetas y sobrecarga de función simple:

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/comprender, se puede expandir sin sudar (y sin repetitivo if cláusulas 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 estándar:

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í es un buen post sobre los conceptos básicos.

  • ¿No agrega tiempo de compilación llamando a bonus() ? ¿O el compilador detecta que bonus está vacío y no ejecuta la llamada de función?

    – plougue

    25 de abril de 2017 a las 9:50

  • bonus<false>() invoca la versión predeterminada del bonus plantilla (líneas 9 y 10 del ejemplo), por lo que no hay llamada de función. Para decirlo de otra manera, MyFunc() compila en un bloque de código (sin condicionales) y MyFunc<true>() compila en un bloque de código diferente (sin condicionales).

    – David K.

    25 de abril de 2017 a las 13:01

  • 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

    25 de abril de 2017 a las 13:56

¿Ha sido útil esta solución?