¿Cómo convierto un doble arbitrario en un entero y evito un comportamiento indefinido?

Digamos que tengo una función que acepta un entero de 64 bits, y quiero llamarlo con un double con valor numérico arbitrario (es decir, puede ser de una magnitud muy grande, o incluso infinita):

 void DoSomething(int64_t x); double d = [...]; DoSomething(d); 

El párrafo 1 de [conv.fpint] en el estándar C ++ 11 dice esto:

Un prvalue de un tipo de punto flotante se puede convertir en un prvalue de un tipo entero. La conversión se trunca; es decir, se desecha la parte fraccionaria. El comportamiento no está definido si el valor truncado no se puede representar en el tipo de destino.

Por lo tanto, hay muchos valores de d anteriores que causarán un comportamiento indefinido. Me gustaría que la conversión se sature, para que los valores mayores que std::numeric_limits::max() (llamados kint64max continuación), incluido el infinito, se conviertan en ese valor, y de manera similar con el valor mínimo representable. Este parece ser el enfoque natural:

 double clamped = std::min(d, static_cast(kint64max)); clamped = std::max(clamped, static_cast(kint64min)); DoSomething(clamped); 

Pero, el siguiente párrafo en el estándar dice esto:

Un prvalue de un tipo entero o de un tipo de enumeración sin ámbito se puede convertir en un prvalue de un tipo de punto flotante. El resultado es exacto si es posible. Si el valor que se está convirtiendo está en el rango de valores que se pueden representar pero el valor no se puede representar exactamente, es una elección definida por la implementación del siguiente valor representable más bajo o más alto.

Por lo tanto, es posible que el kint64max + 1 todavía kint64max + 1 siendo kint64max + 1 , y que el comportamiento aún no esté definido.

¿Cuál es la forma portátil más sencilla de hacer lo que estoy buscando? Puntos de bonificación si también maneja con gracia NaN s.

Actualización : para ser más precisos, me gustaría que todo lo siguiente fuera cierto de una función int64_t SafeCast(double) que resuelve este problema:

  1. Para cualquier doble d , la llamada a SafeCast(d) no realiza un comportamiento indefinido de acuerdo con el estándar, ni lanza una excepción o anula.

  2. Para cualquier doble d en el rango [-2^63, 2^63) , SafeCast(d) == static_cast(d) . Es decir, SafeCast está de acuerdo con las reglas de conversión de C ++ siempre que se defina esta última.

  3. Para cualquier doble d >= 2^63 , SafeCast(d) == kint64max .

  4. Para cualquier doble d < -2^63 , SafeCast(d) == kint64min .

Sospecho que la verdadera dificultad aquí es averiguar si d está en el rango [-2^63, 2^63) . Como se discutió en la pregunta y en los comentarios a otras respuestas, creo que usar un modelo de kint64max para double y probar el límite superior no es un motor de arranque debido a un comportamiento indefinido. Puede ser más prometedor usar std::pow(2, 63) , pero no sé si se garantiza que esto sea exactamente 2 ^ 63.

Resulta que esto es más fácil de hacer de lo que pensaba. Gracias a Michael O’Reilly por la idea básica de esta solución.

El meollo del asunto es determinar si el doble truncado se podrá representar como int64_t . Puedes hacerlo fácilmente usando std::frexp :

 #include  #include  static constexpr int64_t kint64min = std::numeric_limits::min(); static constexpr int64_t kint64max = std::numeric_limits::max(); int64_t SafeCast(double d) { // We must special-case NaN, for which the logic below doesn't work. if (std::isnan(d)) { return 0; } // Find that exponent exp such that // d == x * 2^exp // for some x with abs(x) in [0.5, 1.0). Note that this implies that the // magnitude of d is strictly less than 2^exp. // // If d is infinite, the call to std::frexp is legal but the contents of exp // are unspecified. int exp; std::frexp(d, &exp); // If the magnitude of d is strictly less than 2^63, the truncated version // of d is guaranteed to be representable. The only representable integer // for which this is not the case is kint64min, but it is covered by the // logic below. if (std::isfinite(d) && exp <= 63) { return d; } // Handle infinities and finite numbers with magnitude >= 2^63. return std::signbit(d) ? kint64min : kint64max; } 

Aquí hay una solución que no cumple con todos los criterios, junto con el análisis de por qué no. Vea la respuesta aceptada para una mejor respuesta.

 // Define constants from the question. static constexpr int64_t kint64min = std::numeric_limits::min(); static constexpr int64_t kint64max = std::numeric_limits::max(); int64_t SafeCast(double d) { // Handle NaN specially. if (std::isnan(d)) return 0; // Handle out of range below. if (d <= kint64min) return kint64min; // Handle out of range above. if (d >= kint64max) return kint64max; // At this point we know that d is in range. return d; } 

Creo que esto evita un comportamiento indefinido. No hay que tener cuidado con la conversión de enteros a dobles en los controles de rango. Asumiendo la cordura en la forma en que se convierten los enteros no representables (en particular que el mapeo es monótono), en el momento en que las verificaciones de rango hayan pasado, podemos estar seguros de que d está en [-2^63, 2^63) , como se requiere para el lanzamiento implícito al final de la función.

También estoy seguro de que esto supera los valores de rango correctamente.

El problema es el criterio # 2 de la actualización a mi pregunta. Considere una implementación donde kint64max no se kint64max representar como un doble, pero kint64max - 1 es. Además, suponga que esta es una implementación en la que kint64max a un doble produce el siguiente valor representable más bajo, es decir, kint64max - 1 . Sea d 2 2 63 (es decir, kint64max - 1 ). Entonces, SafeCast(d) es kint64max , porque la verificación de rango convierte a kint64max en un doble, dando un valor igual a d . Pero static_cast(d) es kint64max - 1 .

Por más que lo intente, no puedo encontrar una manera de resolver esto. Tampoco puedo escribir una prueba de unidad que verifique mis criterios, sin que la prueba de unidad ejecute un comportamiento indefinido. Siento que hay una lección más profunda que aprender aquí, algo acerca de la imposibilidad de detectar si una acción en un sistema causará un comportamiento indefinido desde dentro del mismo sistema, sin causar un comportamiento indefinido.

Qué tal si:

 constexpr uint64_t weird_high_limit = (double)kint64max == (double)(kint64max-1); int64_t clamped = (d >= weird_high_limit + kint64max)? kint64max: (d <= kint64min)? kint64min: int64_t(d); 

Creo que esto se encarga de todos los casos de vanguardia. Si d < (double)kint64max , entonces (exact)d <= (exact)kint64max . La prueba procede por la contradicción del hecho de que (double)kint64max es el siguiente valor representable más alto o más bajo.