Le stockage std :: chrono :: years est-il vraiment au moins 17 bits?

14

De cppreference

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

L' utilisation libc++, il semble que le stockage sous - jacent de std::chrono::yearsest -ce shortqui est signé 16 bits .

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

Y a-t-il une faute de frappe sur cppreference ou autre chose?

Exemple:

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

[ Lien Godbolt ]

sandthorn
la source
2
yearplage: eel.is/c++draft/time.cal.year#members-19 years plage: eel.is/c++draft/time.syn . yearest le "nom" de l'année civile et nécessite 16 bits. yearsest une durée chrono, pas la même chose qu'un year. On peut soustraire deux yearet le résultat a du type years. yearsest nécessaire pour pouvoir conserver le résultat de year::max() - year::min().
Howard Hinnant
1
std::chrono::years( 30797 ) + 365dne compile pas.
Howard Hinnant
1
Le résultat years{30797} + days{365}est de 204528013 avec des unités de 216s.
Howard Hinnant
1
C'est juste deux durées ajoutées. Interdire cela signifierait interdire hours{2} + seconds{5}.
Howard Hinnant
4
Je pense que vous confondez composants calendrical avec les types de durée , car ils n'ont des noms similaires. Voici une règle générale: les noms sont plurielles: , , . Noms de composants Calendrical sont singuliers: , , . est une erreur de compilation. est cette année. est une durée de 2020 ans. durationyearsmonthsdaysyearmonthdayyear{30797} + day{365}year{2020}years{2020}
Howard Hinnant

Réponses:

8

L'article cppreference est correct . Si libc ++ utilise un type plus petit, cela semble être un bogue dans libc ++.

Andrey Semashev
la source
Mais en ajouter un autre wordqui est probablement à peine utilisé ne serait-il pas year_month_dayinutile de gonfler les vecteurs? Cela at least 17 bitsne pourrait- il pas être considéré comme un texte norminal?
Sandthorn
3
@sandthorn year_month_daycontient year, non years. La représentation de yearn'est pas nécessairement 16 bits, bien que le type shortsoit utilisé comme exposition. OTOH, la partie 17 bits de la yearsdéfinition est normative car elle n'est pas marquée comme exposition uniquement. Et franchement, dire qu'il s'agit d'au moins 17 bits et ne pas l'exiger n'a aucun sens.
Andrey Semashev
1
Ah yearen year_month_daysemble être en inteffet. => operator int Je pense que cela prend at least 17 bits yearsen charge la mise en œuvre.
Sandthorn
Pourriez-vous modifier votre réponse? Il s'avère que std :: chrono :: years est en fait int et std :: chrono :: year est max à 32767 arbitrairement ..
sandthorn
@sandthorn La réponse est correcte, je ne vois pas pourquoi je devrais l'éditer.
Andrey Semashev
4

Je décompose l'exemple sur https://godbolt.org/z/SNivyp morceau par morceau:

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

Simplifier et supposer using namespace std::chronoest dans la portée:

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

La sous-expression years{0}est un durationavec un periodégal à ratio<31'556'952>et une valeur égale à 0. Notez que years{1}, exprimé en virgule flottante days, est exactement 365,2425. Il s'agit de la durée moyenne de l'année civile.

La sous-expression days{365}est un durationavec un periodégal à ratio<86'400>et une valeur égale à 365.

La sous-expression years{0} + days{365}est un durationavec un periodégal à ratio<216>et une valeur égale à 146'000. Ceci est formé en trouvant d'abord le common_type_tde ratio<31'556'952>et ratio<86'400>qui est le GCD (31'556'952, 86'400), ou 216. La bibliothèque convertit d'abord les deux opérandes en cette unité commune, puis fait l'addition dans l'unité commune.

Pour convertir years{0}en unités avec une période de 216s, il faut multiplier 0 par 146'097. Cela se trouve être un point très important. Cette conversion peut facilement provoquer un débordement lorsqu'elle n'est effectuée qu'avec 32 bits.

<côté>

Si, à ce stade, vous vous sentez confus, c'est parce que le code a probablement l'intention d'un calcul calendaire , mais fait en fait un calcul chronologique . Les calculs calendaires sont des calculs avec des calendriers.

Les calendriers présentent toutes sortes d'irrégularités, telles que des mois et des années ayant des longueurs physiques différentes en termes de jours. Un calcul calendaire prend en compte ces irrégularités.

Un calcul chronologique fonctionne avec des unités fixes et ne fait que lancer les chiffres sans tenir compte des calendriers. Un calcul chronologique ne se soucie pas si vous utilisez le calendrier grégorien, le calendrier julien, le calendrier hindou, le calendrier chinois, etc.

</aside>

Ensuite, nous prenons notre 146000[216]sdurée et la convertissons en une durée avec un periodde ratio<86'400>(qui a un alias de type nommé days). La fonction floor<days>()effectue cette conversion et le résultat est 365[86400]s, ou plus simplement, juste 365d.

L'étape suivante prend le durationet le convertit en a time_point. Le type du time_pointest time_point<system_clock, days>qui a un alias de type nommé sys_days. Il s'agit simplement d'un décompte daysdepuis l' system_clocképoque, qui est 1970-01-01 00:00:00 UTC, à l'exclusion des secondes intercalaires.

Enfin, le sys_daysest converti en un year_month_dayavec la valeur 1971-01-01.

Un moyen plus simple de faire ce calcul est:

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

Considérez ce calcul similaire:

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

Il en résulte la date 16668-12-31. Ce qui est probablement un jour plus tôt que prévu ((14699 + 1970) -01-01). Le sous - expression years{14699} + days{0}est maintenant: 2'147'479'803[216]s. Notez que la valeur d'exécution est proche de INT_MAX( 2'147'483'647) et que le sous-jacent repdes deux yearset daysest int.

En effet , si vous convertissez years{14700}des unités de [216]svous overflow: -2'147'341'396[216]s.

Pour résoudre ce problème, passez à un calcul calendaire:

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

Tous les résultats sur https://godbolt.org/z/SNivyp qui ajoutent yearset daysutilisent une valeur yearssupérieure à 14699 connaissent un intdébordement.

Si l'on veut vraiment faire des calculs chronologiques avec yearset de dayscette façon, il serait sage d'utiliser l'arithmétique 64 bits. Cela peut être accompli en convertissant yearsen unités avec une reputilisation supérieure à 32 bits au début du calcul. Par exemple:

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

En ajoutant 0sà years, ( secondsdoit avoir au moins 35 bits), puis common_type repest forcé à 64 bits pour le premier ajout ( years{14700} + 0s) et continue en 64 bits lors de l'ajout days{0}:

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

Encore une autre façon d'éviter un débordement intermédiaire (dans cette plage) consiste à tronquer yearsavec daysprécision avant d'en ajouter plus days:

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

ja la valeur 16669-12-31. Cela évite le problème car maintenant l' [216]sunité n'est jamais créée en premier lieu. Et nous ne nous approchons même pas de la limite pour years, daysou year.

Bien que si vous vous y attendiez 16700-01-01, vous avez toujours un problème, et la façon de le corriger est de faire un calcul calendaire à la place:

year_month_day j = (1970y + years{14700})/1/1;
Howard Hinnant
la source
1
Grande explication. Je m'inquiète du calcul chronologique. Si je vois years{14700} + 0s + days{0}dans une base de code, je n'aurais aucune idée de ce 0squi s'y fait et de son importance. Existe-t-il un autre moyen, peut-être plus explicite? Souhaitez-vous que quelque chose de duration_cast<seconds>(years{14700}) + days{0}mieux?
bolov
duration_castserait pire car il est de mauvaise forme à utiliser duration_castpour les conversions non tronquées. La troncature des conversions peut être une source d'erreurs logiques, et il est préférable de n'utiliser le "gros marteau" que lorsque vous en avez besoin, afin de pouvoir facilement repérer les conversions tronquées dans votre code.
Howard Hinnant
1
On pourrait créer une durée personnalisée:, use llyears = duration<long long, years::period>;puis l'utiliser à la place. Mais la meilleure chose à faire est probablement de réfléchir à ce que vous essayez d'accomplir et de vous demander si vous vous y prenez correctement. Par exemple, avez-vous vraiment besoin d'une précision journalière sur une échelle de temps de 10 000 ans? Le calendrier civil n'est exact qu'à environ 1 jour en 4 000 ans. Peut-être qu'un millénaire en virgule flottante serait une meilleure unité?
Howard Hinnant
Clarification: la modélisation chrono du calendrier civil est exacte dans la plage -32767/1/1 à 32767/12/31. La précision du calendrier civil en ce qui concerne la modélisation du système solaire n'est que d'environ 1 jour en 4 000 ans.
Howard Hinnant
1
Cela dépendrait vraiment du cas d'utilisation et j'ai actuellement du mal à penser à un cas d'utilisation motivant à ajouter yearset days. Cela ajoute littéralement un multiple de 365,2425 jours à un certain nombre entier de jours. Normalement si vous voulez faire un calcul chronologique de l'ordre des mois ou des années, c'est pour modéliser de la physique ou de la biologie. Peut-être que ce post sur les différentes façons d'ajouter monthsà system_clock::time_pointcontribuerait à clarifier la différence entre les deux types de calculs: stackoverflow.com/a/43018120/576911
Howard Hinnant