La forma más elegante de escribir un 'si'

137

Desde C ++ 17 se puede escribir un ifbloque que se ejecutará exactamente una vez como esta:

#include <iostream>
int main() {
    for (unsigned i = 0; i < 10; ++i) {

        if (static bool do_once = true; do_once) { // Enter only once
            std::cout << "hello one-shot" << std::endl;
            // Possibly much more code
            do_once = false;
        }

    }
}

Sé que podría estar pensando demasiado en esto, y hay otras formas de resolver esto, pero aún así, ¿es posible escribir esto de alguna manera como esta, por lo que no hay necesidad del do_once = falsefinal?

if (DO_ONCE) {
    // Do stuff
}

Estoy pensando en una función auxiliar do_once(), que contiene el static bool do_once, pero ¿qué pasa si quisiera usar esa misma función en diferentes lugares? ¿Podría ser este el momento y el lugar para un #define? Espero que no.

nada
fuente
52
¿Por qué no solo if (i == 0)? Está lo suficientemente claro.
SilvanoCerza
26
@SilvanoCerza Porque ese no es el punto. Este if-block podría estar en algún lugar de alguna función que se ejecuta varias veces, no en un bucle regular
nada
8
quizás std::call_oncees una opción (se usa para enhebrar, pero aún así funciona).
fdan
25
Su ejemplo puede ser un mal reflejo de su problema del mundo real que no nos está mostrando, pero ¿por qué no simplemente eliminar la función de llamada única del bucle?
rubenvb
14
No se me ocurrió que las variables inicializadas en una ifcondición podrían ser static. Eso es inteligente.
HolyBlackCat

Respuestas:

144

Uso std::exchange:

if (static bool do_once = true; std::exchange(do_once, false))

Puede acortarlo invirtiendo el valor de verdad:

if (static bool do_once; !std::exchange(do_once, true))

Pero si está usando esto mucho, no sea elegante y cree un envoltorio en su lugar:

struct Once {
    bool b = true;
    explicit operator bool() { return std::exchange(b, false); }
};

Y úsalo como:

if (static Once once; once)

No se debe hacer referencia a la variable fuera de la condición, por lo que el nombre no nos compra mucho. Inspirándonos en otros lenguajes como Python que le dan un significado especial al _identificador, podemos escribir:

if (static Once _; _)

Mejoras adicionales: aproveche la sección BSS (@Deduplicator), evite la escritura en la memoria cuando ya la hemos ejecutado (@ShadowRanger) y brinde una sugerencia de predicción de rama si va a realizar pruebas muchas veces (por ejemplo, como en la pregunta):

// GCC, Clang, icc only; use [[likely]] in C++20 instead
#define likely(x) __builtin_expect(!!(x), 1)

struct Once {
    bool b = false;
    explicit operator bool()
    {
        if (likely(b))
            return false;

        b = true;
        return true;
    }
};
Bellota
fuente
32
Sé que las macros tienen mucho odio en C ++, pero esto se ve muy limpio: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))para ser utilizado como:ONLY_ONCE { foo(); }
Fibbles
55
Es decir, si usted escribe "una vez" tres veces, y luego lo utilizan más de tres veces en sentencias if vale la pena imo
Alan
13
El nombre _se usa en muchos programas para marcar cadenas traducibles. Esperar que sucedan cosas interesantes.
Simon Richter
1
Si tiene la opción, prefiera que el valor inicial del estado estático sea todo-bits-cero. La mayoría de los formatos ejecutables contienen la longitud de un área completamente cero.
Deduplicador el
77
Usando _para la variable no habría Pythonic. No se utiliza _para las variables que se hará referencia más adelante, sólo para almacenar valores en la que tiene que proporcionar una variable, pero que no es necesario el valor. Por lo general, se usa para desempaquetar cuando solo necesita algunos de los valores. (Hay otros casos de uso, pero son bastante diferentes del caso del valor de usar y tirar).
jpmc26
91

Quizás no sea la solución más elegante y no vea ninguna real if, pero la biblioteca estándar en realidad cubre este caso :, vea std::call_once.

#include <mutex>

std::once_flag flag;

for (int i = 0; i < 10; ++i)
    std::call_once(flag, [](){ std::puts("once\n"); });

La ventaja aquí es que esto es seguro para subprocesos.

lubgr
fuente
3
No estaba al tanto de std :: call_once en ese contexto. Pero con esta solución, debe declarar un std :: once_flag para cada lugar que use ese std :: call_once, ¿no?
nada
11
Esto funciona, pero no fue para una solución simple, la idea es para aplicaciones multiproceso. Es excesivo para algo tan simple porque usa sincronización interna, todo para algo que se resolvería de forma simple si. No estaba pidiendo una solución segura para hilos.
Michael Chourdakis
9
@MichaelChourdakis Estoy de acuerdo contigo, es una exageración. Sin embargo, vale la pena conocerlo, y especialmente conocer la posibilidad de expresar lo que está haciendo ("ejecutar este código una vez") en lugar de ocultar algo detrás de un truco de if menos legible.
lubgr
17
Verme call_oncesignifica que quieres llamar a algo una vez. Loco, lo sé.
Barry
3
@SergeyA porque usa sincronización interna, todo para algo que se resolvería con un simple si. Es una forma bastante idiomática de hacer algo diferente a lo que se le pidió.
Carreras de ligereza en órbita
52

C ++ tiene una primitiva de flujo de control integrada que consiste en "( antes del bloque; condición; después del bloque )" ya:

for (static bool b = true; b; b = false)

O más pirateado, pero más corto:

for (static bool b; !b; b = !b)

Sin embargo, creo que cualquiera de las técnicas presentadas aquí debe usarse con cuidado, ya que no son (¿todavía?) Muy comunes.

Sebastian Mach
fuente
1
Me gusta la primera opción (aunque, como muchas variantes aquí, no es segura para subprocesos, así que tenga cuidado). La segunda opción me da escalofríos (más difícil de leer y se podrá ejecutar cualquier número de veces con sólo dos hilos ... b == false: Thread 1evalúa !by entra por lazo, Thread 2evalúa !by entra por lazo, Thread 1hace sus cosas y se va el bucle, el establecimiento b == falsede !bdecir b = true... Thread 2hace sus cosas y deja el bucle for, estableciéndolo b == trueen !bie b = false, permitiendo que todo el proceso se repita indefinidamente)
CharonX
3
Me parece irónico que una de las soluciones más elegantes para un problema, donde se supone que un código se ejecute solo una vez, sea un bucle . +1
nada
2
Evitaría b=!b, eso se ve bien, pero en realidad quieres que el valor sea falso, por b=falselo que será preferible.
yo
2
Tenga en cuenta que el bloque protegido se ejecutará nuevamente si sale de manera no local. Eso incluso podría ser deseable, pero es diferente de todos los otros enfoques.
Davis Herring
29

En C ++ 17 puedes escribir

if (static int i; i == 0 && (i = 1)){

para evitar jugar con iel cuerpo del bucle. icomienza con 0 (garantizados por la norma), y la expresión después de los ;conjuntos ia 1la primera vez que se evalúa.

Tenga en cuenta que en C ++ 11 podría lograr lo mismo con una función lambda

if ([]{static int i; return i == 0 && (i = 1);}()){

lo que también conlleva una ligera ventaja ya que ino se filtra en el cuerpo del bucle.

Betsabé
fuente
44
Me entristece decir que si se pusiera en un #define llamado CALL_ONCE o sería más legible
nada
9
Si bien static int i;podría (realmente no estoy seguro) ser uno de esos casos en los que ise garantiza que se inicializará 0, es mucho más claro usarlo static int i = 0;aquí.
Kyle Willmon
77
Independientemente de que estoy de acuerdo, un inicializador es una buena idea para la comprensión
Lightness Races in Orbit
55
@Bathsheba Kyle no lo hizo, por lo que su afirmación ya ha demostrado ser falsa. ¿Cuánto le cuesta agregar dos caracteres en aras de un código claro? Vamos, eres un "arquitecto jefe de software"; deberías saber esto :)
Las carreras de ligereza en órbita
55
Si cree que deletrear el valor inicial de una variable es lo contrario de claro, o sugiere que "algo funky está sucediendo", no creo que pueda ser ayudado;)
Carreras de ligereza en órbita
14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Esta solución es segura para subprocesos (a diferencia de muchas de las otras sugerencias).

Waxrat
fuente
3
¿Sabía que ()es opcional (si está vacío) en la declaración lambda?
Nonyme 01 de
9

Puede ajustar la acción de una sola vez en el constructor de un objeto estático que instancia en lugar del condicional.

Ejemplo:

#include <iostream>
#include <functional>

struct do_once {
    do_once(std::function<void(void)> fun) {
        fun();
    }
};

int main()
{
    for (int i = 0; i < 3; ++i) {
        static do_once action([](){ std::cout << "once\n"; });
        std::cout << "Hello World\n";
    }
}

O, de hecho, puede quedarse con una macro, que puede verse así:

#include <iostream>

#define DO_ONCE(exp) \
do { \
  static bool used_before = false; \
  if (used_before) break; \
  used_before = true; \
  { exp; } \
} while(0)  

int main()
{
    for (int i = 0; i < 3; ++i) {
        DO_ONCE(std::cout << "once\n");
        std::cout << "Hello World\n";
    }
}
moooeeeep
fuente
8

Como dijo @damon, puede evitar el uso std::exchangeutilizando un número entero decreciente, pero debe recordar que los valores negativos se resuelven en verdaderos. La forma de usar esto sería:

if (static int n_times = 3; n_times && n_times--)
{
    std::cout << "Hello world x3" << std::endl;
} 

Traducir esto al elegante envoltorio de @ Acorn se vería así:

struct n_times {
    int n;
    n_times(int number) {
        n = number;
    };
    explicit operator bool() {
        return n && n--;
    };
};

...

if(static n_times _(2); _)
{
    std::cout << "Hello world twice" << std::endl;
}
Oreganop
fuente
7

Si bien usar std::exchangesegún lo sugerido por @Acorn es probablemente la forma más idiomática, una operación de intercambio no es necesariamente barata. Aunque, por supuesto, se garantiza que la inicialización estática sea segura para subprocesos (a menos que le indique a su compilador que no lo haga), por lo que cualquier consideración sobre el rendimiento es algo inútil de todos modos en presencia de la staticpalabra clave.

Si usted está preocupado por micro-optimización (como las personas que usan C ++ son a menudo), usted podría así rayar booly utilizar inten su lugar, lo que le permitirá el uso post-decremento (o más bien, de la subasta , ya que a diferencia booldecrementar una intserá no saturar a cero ...):

if(static int do_once = 0; !do_once++)

Solía ​​ser que booltenía operadores de incremento / decremento, pero fueron desaprobados hace mucho tiempo (C ++ 11? No estoy seguro?) Y deben eliminarse por completo en C ++ 17. Sin embargo, puede disminuir una intmulta y, por supuesto, funcionará como una condición booleana.

Bonificación: puede implementar do_twiceo do_thricesimilar ...

Damon
fuente
Probé esto y se dispara varias veces, excepto por primera vez.
nada
@nada: Tonta, tienes razón ... lo corrigió. Solía ​​trabajar booly disminuir una vez. Pero el incremento funciona bien con int. Ver demostración en línea: coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon
1
Esto todavía tiene el problema de que podría ejecutarse tantas veces que se do_onceenvuelve y eventualmente golpeará 0 nuevamente (y una y otra vez ...).
nada
Para ser más precisos: esto ahora se ejecutaría cada INT_MAX veces.
nada
Bueno, sí, pero aparte del contador de bucle que también se envuelve en ese caso, no es un problema. Pocas personas ejecutan iteraciones de 2 mil millones (o 4 mil millones si no están firmadas) de algo. Si lo hacen, aún podrían usar un número entero de 64 bits. Usando la computadora más rápida disponible, morirás antes de que se encienda, por lo que no puedes ser demandado por ello.
Damon
4

Basado en la gran respuesta de @ Bathsheba para esto, simplemente lo hizo aún más simple.

En C++ 17, simplemente puedes hacer:

if (static int i; !i++) {
  cout << "Execute once";
}

(En versiones anteriores, simplemente declara int ifuera del bloque. También funciona en C :)).

En palabras simples: declaras i, que toma el valor predeterminado de cero ( 0). Zero es falsey, por lo tanto, utilizamos el !operador de signo de exclamación ( ) para negarlo. Luego tomamos en cuenta la propiedad de incremento del <ID>++operador, que primero se procesa (asigna, etc.) y luego se incrementa.

Por lo tanto, en este bloque, se inicializará y tendré el valor 0solo una vez, cuando se ejecute el bloque, y luego el valor aumentará. Simplemente usamos el !operador para negarlo.

Nick L.
fuente
1
Si esto hubiera sido publicado anteriormente, probablemente sería la respuesta aceptada ahora. ¡Genial gracias!
nada