¿El almacenamiento std :: chrono :: years es realmente de al menos 17 bits?

14

De cppreference

std::chrono::years (since C++20) duration</*signed integer type of at least 17 bits*/, std::ratio<31556952>>

Usando libc++, parece que el almacenamiento subrayado de std::chrono::yearsis shortestá firmado 16 bits .

std::chrono::years( 30797 )        // yields  32767/01/01
std::chrono::years( 30797 ) + 365d // yields -32768/01/01 apparently UB

¿Hay un error tipográfico en cppreference o algo más?

Ejemplo:

#include <fmt/format.h>
#include <chrono>

template <>
struct fmt::formatter<std::chrono::year_month_day> {
  char presentation = 'F';

  constexpr auto parse(format_parse_context& ctx) {
    auto it = ctx.begin(), end = ctx.end();
    if (it != end && *it == 'F') presentation = *it++;

#   ifdef __exception
    if (it != end && *it != '}') {
      throw format_error("invalid format");
    }
#   endif

    return it;
  }

  template <typename FormatContext>
  auto format(const std::chrono::year_month_day& ymd, FormatContext& ctx) {
    int year(ymd.year() );
    unsigned month(ymd.month() );
    unsigned day(ymd.day() );
    return format_to(
        ctx.out(),
        "{:#6}/{:#02}/{:#02}",
        year, month, day);
  }
};

using days = std::chrono::duration<int32_t, std::ratio<86400> >;
using sys_day = std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<int32_t, std::ratio<86400> >>;

template<typename D>
using sys_time = std::chrono::time_point<std::chrono::system_clock, D>;
using sys_day2 = sys_time<days>;

int main()
{
  auto a = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::hours( (1<<23) - 1 ) 
      )
    )
  );

  auto b = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::minutes( (1l<<29) - 1 ) 
      )
    )
  );

  auto c = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::seconds( (1l<<35) - 1 ) 
      )
    )
  );

  auto e = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::days( (1<<25) - 1 ) 
      )
    )
  );

  auto f = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::weeks( (1<<22) - 1 ) 
      )
    )
  );

  auto g = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::months( (1<<20) - 1 ) 
      )
    )
  );

  auto h = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::years( 30797 ) // 0x7FFF - 1970
      )
    )
  );

  auto i = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::years( 30797 ) // 0x7FFF - 1970
      ) + std::chrono::days(365)
    )
  );

  fmt::print("Calendar limit by duration's underlining storage:\n"
             "23 bit hour       : {:F}\n"
             "29 bit minute     : {:F}\n"
             "35 bit second     : {:F}\n"
             "25 bit days       : {:F}\n"
             "22 bit week       : {:F}\n"
             "20 bit month      : {:F}\n"
             "16? bit year      : {:F}\n"
             "16? bit year+365d : {:F}\n"
             , a, b, c, e, f, g, h, i);
}

[ Enlace de Godbolt ]

espino de arena
fuente
2
yearrango: eel.is/c++draft/time.cal.year#members-19 years rango: eel.is/c++draft/time.syn . yeares el "nombre" del año civil y requiere 16 bits. yearses una duración crono, no es lo mismo que a year. Uno puede restar dos yeary el resultado tiene tipo years. yearsse requiere para poder mantener el resultado de year::max() - year::min().
Howard Hinnant
1
std::chrono::years( 30797 ) + 365dno compila
Howard Hinnant
1
El resultado de years{30797} + days{365}es 204528013 con unidades de 216s.
Howard Hinnant
1
Eso es solo dos duraciones que se agregan. Prohibirlo significaría prohibir hours{2} + seconds{5}.
Howard Hinnant
44
Mi conjetura es que usted está confundiendo componentes del calendario con los tipos de duración, ya que no tienen este tipo de nombres similares. He aquí una regla general: durationlos nombres son plurales: years, months, days. Nombres de los componentes del calendario son singulares: year, month, day. year{30797} + day{365}es un error en tiempo de compilación. year{2020}es este año years{2020}tiene una duración de 2020 años.
Howard Hinnant

Respuestas:

8

El artículo de cppreference es correcto . Si libc ++ usa un tipo más pequeño, entonces esto parece ser un error en libc ++.

Andrey Semashev
fuente
¿Pero agregar otro wordque probablemente apenas se use no sería un year_month_dayvector de volumen innecesario? ¿Podría eso at least 17 bitsno contarse como texto normal?
Sandthorn
3
@sandthorn year_month_daycontiene year, no years. La representación de yearno se requiere que sea de 16 bits, aunque el tipo shortse utiliza como exposición. OTOH, la parte de 17 bits en la yearsdefinición es normativa, ya que no está marcada solo como exposición. Y francamente, decir que tiene al menos 17 bits y luego no requerirlo no tiene sentido.
Andrey Semashev
1
Ah yearen year_month_dayparece ser de inthecho. => operator int Creo que esto es compatible con la at least 17 bits yearsimplementación.
Sandthorn
¿Te importaría editar tu respuesta? Resulta que std :: chrono :: years es en realidad int y std :: chrono :: year es max a 32767 arbitrariamente ..
sandthorn
@sandthorn La respuesta es correcta, no veo por qué tendría que editarlo.
Andrey Semashev
4

Estoy desglosando el ejemplo en https://godbolt.org/z/SNivyp pieza por pieza:

  auto a = std::chrono::year_month_day( 
    sys_days( 
      std::chrono::floor<days>(
        std::chrono::years(0) 
        + std::chrono::days( 365 )
      )
    )
  );

Simplificar y asumir using namespace std::chronoestá dentro del alcance:

year_month_day a = sys_days{floor<days>(years{0} + days{365})};

La subexpresión years{0}es un durationcon un valor periodigual a ratio<31'556'952>y un valor igual a 0. Tenga en cuenta que years{1}, expresado como punto flotante days, es exactamente 365.2425. Esta es la duración promedio del año civil.

La subexpresión days{365}es un durationcon un valor periodigual a ratio<86'400>y un valor igual a 365.

La subexpresión years{0} + days{365}es un durationcon un valor periodigual a ratio<216>y un valor igual a 146'000. Esto se forma al encontrar primero el common_type_tde ratio<31'556'952>y ratio<86'400>cuál es el MCD (31'556'952, 86'400), o 216. La biblioteca primero convierte ambos operandos a esta unidad común, y luego hace la suma en la unidad común.

Para convertir years{0}a unidades con un período de 216s, se debe multiplicar 0 por 146'097. Esto pasa a ser un punto muy importante. Esta conversión puede causar desbordamiento fácilmente cuando se realiza con solo 32 bits.

<aside>

Si en este punto te sientes confundido, es porque el código probablemente intenta un cálculo calendárico , pero en realidad está haciendo un cálculo cronológico . Los cálculos calendáricos son cálculos con calendarios.

Los calendarios tienen todo tipo de irregularidades, como meses y años de diferente duración física en términos de días. Un cálculo calendárico tiene en cuenta estas irregularidades.

Un cálculo cronológico funciona con unidades fijas, y solo produce los números sin tener en cuenta los calendarios. A un cálculo cronológico no le importa si usa el calendario gregoriano, el calendario juliano, el calendario hindú, el calendario chino, etc.

</aside>

Luego tomamos nuestra 146000[216]sduración y la convertimos a una duración con un periodof ratio<86'400>(que tiene un alias de tipo llamado days). La función floor<days>()realiza esta conversión y el resultado es 365[86400]s, o más simplemente, justo 365d.

El siguiente paso toma el durationy lo convierte en a time_point. El tipo de time_pointes el time_point<system_clock, days>que tiene un alias de tipo llamado sys_days. Esto es simplemente un recuento daysdesde la system_clocképoca, que es 1970-01-01 00:00:00 UTC, excluyendo los segundos bisiestos.

Finalmente sys_daysse convierte a a year_month_daycon el valor 1971-01-01.

Una forma más simple de hacer este cálculo es:

year_month_day a = sys_days{} + days{365};

Considere este cálculo similar:

year_month_day j = sys_days{floor<days>(years{14699} + days{0})};

Esto da como resultado la fecha 16668-12-31. Probablemente un día antes de lo que esperaba ((14699 + 1970) -01-01). La subexpresión years{14699} + days{0}es ahora: 2'147'479'803[216]s. Tenga en cuenta que el valor de tiempo de ejecución se está acercando INT_MAX( 2'147'483'647) y que el subyacente repde ambos yearsy dayses int.

De hecho si convierte years{14700}a unidades de [216]sllegar desbordamiento: -2'147'341'396[216]s.

Para solucionar esto, cambie a un cálculo calendárico:

year_month_day j = (1970y + years{14700})/1/1;

Todos los resultados en https://godbolt.org/z/SNivyp que están agregando yearsy daysusando un valor yearsmayor que 14699 están experimentando un intdesbordamiento.

Si uno realmente quiere hacer cálculos cronológicos con yearsy de daysesta manera, entonces sería aconsejable usar aritmética de 64 bits. Esto se puede lograr mediante la conversión yearsa unidades con un repuso superior a 32 bits al principio del cálculo. Por ejemplo:

years{14700} + 0s + days{0}

Al agregar 0sa years, ( secondsdebe tener al menos 35 bits), luego common_type repse fuerza a 64 bits para la primera adición ( years{14700} + 0s) y continúa en 64 bits al agregar days{0}:

463'887'194'400s == 14700 * 365.2425 * 86400

Otra forma de evitar el desbordamiento intermedio (en este rango) es truncar yearscon daysprecisión antes de agregar más days:

year_month_day j = sys_days{floor<days>(years{14700})} + days{0};

jtiene el valor 16669-12-31. Esto evita el problema porque ahora la [216]sunidad nunca se crea en primer lugar. Y nunca nos acercamos al límite de years, dayso year.

Aunque si esperaba 16700-01-01, todavía tiene un problema, y ​​la forma de corregirlo es hacer un cálculo calendárico en su lugar:

year_month_day j = (1970y + years{14700})/1/1;
Howard Hinnant
fuente
1
Gran explicación Estoy preocupado por el cálculo cronológico. Si veo years{14700} + 0s + days{0}en una base de código, no tendría idea de lo que 0sestá haciendo allí y lo importante que es. ¿Existe una forma alternativa, tal vez más explícita? ¿Te gustaría algo duration_cast<seconds>(years{14700}) + days{0}mejor?
bolov
duration_castsería peor porque es una mala forma de usar duration_castpara conversiones no truncadas. Truncar las conversiones puede ser una fuente de errores lógicos, y es mejor usar solo el "gran martillo" cuando lo necesite, para que pueda detectar fácilmente las conversiones truncadas en su código.
Howard Hinnant
1
Se podría crear una duración personalizada: use llyears = duration<long long, years::period>;y luego usarla en su lugar. Pero probablemente lo mejor es pensar en lo que está tratando de lograr y preguntarse si lo está haciendo de la manera correcta. Por ejemplo, ¿realmente necesita precisión de día en una escala de tiempo de 10 mil años? El calendario civil solo tiene una precisión de aproximadamente 1 día en 4 mil años. ¿Quizás un milenio en coma flotante sería una mejor unidad?
Howard Hinnant
Aclaración: el modelado de crono del calendario civil es exacto en el rango -32767/1/1 a 32767/12/31. La precisión del calendario civil con respecto al modelado del sistema solar es de solo 1 día en 4 mil años.
Howard Hinnant
1
Realmente dependería del caso de uso y actualmente tengo problemas para pensar en un caso de uso motivador para agregar yearsy days. Esto literalmente está agregando un múltiplo de 365.2425 días a un número integral de días. Normalmente, si desea hacer un cálculo cronológico en el orden de meses o años, es modelar algo de física o biología. Tal vez este post sobre las diferentes maneras de añadir monthsa system_clock::time_pointayudaría a aclarar la diferencia entre los dos tipos de cálculos: stackoverflow.com/a/43018120/576911
Howard Hinnant