detección de la multiplicación de enteros uint64_t desborda con C

¿Hay alguna forma eficiente y portátil de verificar cuándo las operaciones de multiplicación con int64_t o uint64_t se desbordan en C?

Por ejemplo, para agregar uint64_t puedo hacer:

if (UINT64_MAX - a < b) overflow_detected();
else sum = a + b;

Pero no puedo llegar a una expresión simple similar para la multiplicación.

Todo lo que se me ocurre es dividir los operandos en partes uint32_t altas y bajas y realizar la multiplicación de esas partes mientras se verifica el desbordamiento, algo realmente feo y probablemente también ineficiente.

Update 1: Algún código de referencia que implementa varios enfoques agregados

Update 2: Método Jens Gustedt agregado

benchmarking program:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#define N 100000000

int d = 2;

#define POW_2_64 ((double)(1 << 31) * (double)(1 << 31) * 4)

#define calc_b (a + c)
// #define calc_b (a + d)

int main(int argc, char *argv[]) {
    uint64_t a;
    uint64_t c = 0;
    int o = 0;
    int opt;

    if (argc != 2) exit(1);

    opt = atoi(argv[1]);

    switch (opt) {

    case 1: /* faked check, just for timing */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            if (c > a) o++;
            c += b * a;
        }
        break;

    case 2: /* using division */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            if (b && (a > UINT64_MAX / b)) o++;
            c += b * a;
        }
        break;

    case 3: /* using floating point, unreliable */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            if ((double)UINT64_MAX < (double)a * (double)b) o++;
            c += b * a;
        }
        break;

    case 4: /* using floating point and division for difficult cases */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            double m = (double)a * (double)b;
            if ( ((double)(~(uint64_t)(0xffffffff)) < m ) &&
                 ( (POW_2_64 < m) ||
                   ( b &&
                     (a > UINT64_MAX / b) ) ) ) o++;
            c += b * a;
        }
        break;

    case 5: /* Jens Gustedt method */
        for (a = 0; a < N; a++) {
            uint64_t b = a + c;
            uint64_t a1, b1;
            if (a > b) { a1 = a; b1 = b; }
            else       { a1 = b; b1 = a; }
            if (b1 > 0xffffffff) o++;
            else {
                uint64_t a1l = (a1 & 0xffffffff) * b1;
                uint64_t a1h = (a1 >> 32) * b1 + (a1l >> 32);
                if (a1h >> 32) o++;
            }
            c += b1 * a1;
        }
        break;

    default:
        exit(2);
    }
    printf("c: %lu, o: %u\n", c, o);
}

Hasta ahora, el caso 4 que usa punto flotante para filtrar la mayoría de los casos es el más rápido cuando se supone que los desbordamientos son muy inusuales, al menos en mi computadora, donde es solo dos veces más lento que el caso de no hacer nada.

Case 5, es un 30% más lento que 4, pero siempre funciona igual, no hay números de casos especiales que requieran un procesamiento más lento como sucede con 4.

Respuestas a la pregunta(10)

Su respuesta a la pregunta