O armazenamento std :: chrono :: years é realmente pelo menos 17 bits?

14

Da cppreference

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

Usando libc++, parece que o armazenamento de sublinhado de std::chrono::yearsé shortque é assinado 16 bits .

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

Existe um erro de digitação na cppreference ou qualquer outra coisa?

Exemplo:

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

[ Link Godbolt ]

espinheiro
fonte
2
yearintervalo: eel.is/c++draft/time.cal.year#members-19 years range: eel.is/c++draft/time.syn . yearé o "nome" do ano civil e requer 16 bits. yearsé uma duração de crono, não é a mesma coisa que a year. Pode-se subtrair dois yeare o resultado tem tipo years. yearsé necessário para poder manter o resultado de year::max() - year::min().
Howard Hinnant 13/03
11
std::chrono::years( 30797 ) + 365dnão compila.
Howard Hinnant 13/03
11
O resultado years{30797} + days{365}é 204528013 com unidades de 216s.
Howard Hinnant 13/03
11
São apenas duas durações sendo adicionadas. Proibir isso significaria proibir hours{2} + seconds{5}.
Howard Hinnant 14/03
4
Meu palpite é que você está confundindo componentes de calendário com tipos de duração, porque eles não têm nomes semelhantes. Aqui está uma regra geral: durationos nomes são plurais: years, months, days. Os nomes dos componentes do calendário são singulares: year , month, day. year{30797} + day{365}é um erro em tempo de compilação. year{2020}é este ano. years{2020}é uma duração de 2020 anos.
Howard Hinnant 14/03

Respostas:

8

O artigo cppreference está correto . Se a libc ++ usa um tipo menor, isso parece ser um bug na libc ++.

Andrey Semashev
fonte
Mas adicionar outro wordque provavelmente mal fosse usado não seria um volume desnecessário de year_month_dayvetores? Isso at least 17 bitsnão pode ser contado como texto norminal?
sandthorn 13/03
3
@sandthorn year_month_daycontém year, não years. A representação de yearnão precisa ser de 16 bits, embora o tipo shortseja usado como exposição. OTOH, a parte de 17 bits na yearsdefinição é normativa, pois não é marcada apenas como exposição. E, francamente, dizer que ele tem pelo menos 17 bits e depois não exigir isso não faz sentido.
Andrey Semashev 13/03
11
Ah yearnos year_month_dayparece ser int, de fato. => operator int Acho que isso suporta a at least 17 bits yearsimplementação.
sandthorn 13/03
Você se importaria de editar sua resposta? Acontece que std :: chrono :: years é realmente int e std :: chrono :: year é máximo em 32767 arbitrariamente ..
sandthorn
@sandthorn A resposta está correta, não vejo por que precisaria editá-la.
Andrey Semashev 17/03
4

Estou detalhando o exemplo em https://godbolt.org/z/SNivyp, peça por peça:

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

Simplificar e assumir using namespace std::chronoestá no escopo:

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

A sub-expressão years{0}é a durationcom um periodigual a ratio<31'556'952>e um valor igual a 0. Observe queyears{1} , expresso como ponto flutuante days, é exatamente 365,2425. Essa é a média duração do ano civil.

A sub-expressão days{365}é a durationcom umperiod igual a ratio<86'400>e um valor igual a365 .

A sub-expressão years{0} + days{365}é a durationcom um periodigual a ratio<216>e um valor igual a 146'000. Isso é formado pela primeira descoberta common_type_tde ratio<31'556'952>eratio<86'400> que é o GCD (31'556'952, 86'400), ou 216. A biblioteca primeiros convertidos ambos os operandos para esta unidade comum, e em seguida faz a adição na unidade comum.

Converter years{0} em unidades com um período de 216s, é necessário multiplicar 0 por 146'97. Este é um ponto muito importante. Essa conversão pode facilmente causar estouro quando feita com apenas 32 bits.

<lado>

Se nesse momento você se sentir confuso, é porque o código provavelmente pretende uma computação de calendário , mas na verdade está fazendo um computação cronológica . Cálculos de calendários são cálculos com calendários.

Os calendários têm todos os tipos de irregularidades, como meses e anos com diferentes comprimentos físicos em termos de dias. Uma computação de calendário leva em consideração essas irregularidades.

Um cálculo cronológico funciona com unidades fixas e apenas aumenta os números sem levar em consideração os calendários. Um cálculo cronológico não se importa se você usa o calendário gregoriano, o calendário juliano, o calendário hindu, o calendário chinês etc.

</aside>

Em seguida, pegamos nossa 146000[216]sduração e a convertemos em uma duração com um periodde ratio<86'400>(que tem um alias de tipo chamado days). A função floor<days>()faz essa conversão e o resultado é 365[86400]s, ou mais simplesmente, apenas 365d.

O próximo passo pega durationoe converte em um time_point. O tipo de time_pointé o time_point<system_clock, days>que possui um alias de tipo chamado sys_days. Isso é simplesmente uma contagem daysdesde a system_clocképoca, que é 01-01-2009 00:00:00 UTC, excluindo segundos bissextos.

Finalmente, o sys_daysé convertido para a year_month_daycom o valor1971-01-01 .

Uma maneira mais simples de fazer esse cálculo é:

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

Considere este cálculo semelhante:

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

Isso resulta na data 16668-12-31. O que provavelmente é um dia antes do que você esperava ((14699 + 1970) -01-01). A sub-expressão years{14699} + days{0}é agora: 2'147'479'803[216]s. Observe que o valor do tempo de execução está próximo INT_MAX( 2'147'483'647) e que o subjacente repde ambos yearse daysé int.

Na verdade se você converter years{14700}para unidades de [216]svocê começa overflow: -2'147'341'396[216]s.

Para corrigir isso, mude para um cálculo de calendário:

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

Todos os resultados em https://godbolt.org/z/SNivyp que estão adicionando yearse dayse usando um valor para o yearsque é maior do que 14.699 estão experimentandoint estouro.

Se alguém realmente deseja fazer cálculos cronológicos yearse daysdessa maneira, seria aconselhável usar a aritmética de 64 bits. Isso pode ser conseguido convertendo yearspara unidades com um repuso maior que 32 bits no início da computação. Por exemplo:

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

Ao adicionar 0sa years(( secondsdeve ter pelo menos 35 bits), o valor common_type repé forçado a 64 bits para a primeira adição ( years{14700} + 0s) e continua em 64 bits ao adicionar days{0}:

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

Outra maneira de evitar o estouro intermediário (nesse intervalo) é truncar yearscom daysprecisão antes de adicionar mais days:

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

jtem o valor 16669-12-31. Isso evita o problema, porque agora a [216]sunidade nunca é criada em primeiro lugar. E nunca chegamos perto do limite para years, daysou year.

Embora se você estivesse esperando 16700-01-01, ainda tenha um problema, e a maneira de corrigi-lo é fazer um cálculo de calendário:

year_month_day j = (1970y + years{14700})/1/1;
Howard Hinnant
fonte
11
Ótima explicação. Estou preocupado com o cálculo cronológico. Se eu years{14700} + 0s + days{0}vir em uma base de código, não teria idéia do que 0sestá fazendo lá e de quão importante é. Existe uma maneira alternativa, talvez mais explícita? Algo como duration_cast<seconds>(years{14700}) + days{0}seria melhor?
bolov 16/03
duration_castseria pior porque é uma má forma de usar duration_castpara conversões não truncadas. Truncar conversões pode ser uma fonte de erros lógicos, e é melhor usar apenas o "big hammer" quando você precisar, para poder identificar facilmente as conversões truncantes em seu código.
Howard Hinnant 16/03
11
Pode-se criar uma duração personalizada: use llyears = duration<long long, years::period>;e depois usá-la. Mas provavelmente o melhor é pensar no que você está tentando realizar e questionar se está fazendo o caminho certo. Por exemplo, você realmente precisa da precisão do dia em uma escala de tempo de 10 mil anos? O calendário civil tem precisão de apenas 1 dia em 4 mil anos. Talvez um milênio de ponto flutuante seria uma unidade melhor?
Howard Hinnant 16/03
Esclarecimento: a modelagem do calendário civil pela chrono é exata no intervalo de -32767/1/1 a 32767/12/31. A precisão do calendário civil em relação à modelagem do sistema solar é de apenas 1 dia em 4 mil anos.
Howard Hinnant 16/03
11
Isso realmente depende do caso de uso e, atualmente, estou tendo problemas para pensar em um caso de uso motivador para adicionar yearse days. Isso literalmente adiciona alguns múltiplos de 365.2425 dias a um número inteiro de dias. Normalmente, se você deseja fazer um cálculo cronológico da ordem de meses ou anos, é para modelar alguma física ou biologia. Talvez este post sobre as diferentes maneiras de adicionar monthsa system_clock::time_pointajudaria a esclarecer a diferença entre os dois tipos de cálculos: stackoverflow.com/a/43018120/576911
Howard Hinnant