Uygulama tanımlı davranıştan kaçınarak etkili imzasız-imzalı çevrim


94

Bir unsigned intbağımsız değişken alan ve bağımsız değişkene intUINT_MAX + 1 uyumlu bir modulo döndüren bir işlev tanımlamak istiyorum .

İlk deneme şuna benzeyebilir:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

Ancak herhangi bir dil avukatının bildiği gibi, INT_MAX'tan daha büyük değerler için imzasızdan imzalıya geçiş uygulama tanımlıdır.

Bunu, (a) yalnızca şartnamenin gerektirdiği davranışa dayanacak şekilde uygulamak istiyorum; ve (b) herhangi bir modern makinede ve optimize edici derleyicide işlemsiz olarak derlenir.

Garip makinelere gelince ... İmzasız int ile UINT_MAX + 1 uyumlu bir int uyumlu modulo yoksa, diyelim ki bir istisna atmak istiyorum. Birden fazla varsa (bunun mümkün olduğundan emin değilim), diyelim ki en büyüğünü istiyorum.

Tamam, ikinci deneme:

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

Benim mütevazı görüşüme göre bu pek olası olmadığı için, tipik bir ikili tamamlayıcı sistemde olmadığım zaman verimliliği pek umursamıyorum. Ve eğer kodum 2050'nin her yerde mevcut olan işaret büyüklüğü sistemlerinde bir darboğaz haline gelirse, bahse girerim ki birisi bunu anlayabilir ve o zaman optimize edebilir.

Şimdi, bu ikinci deneme benim istediğim şeye oldukça yakın. intBazı girdiler için dönüştürme uygulama tanımlı olsa da ,unsigned modulo UINT_MAX + 1 değerini korumak için standart tarafından garanti edilir. Yani koşullu, tam olarak ne istediğimi kontrol ediyor ve karşılaşabileceğim herhangi bir sistemde hiçbir şeye derlenmeyecek.

Ancak ... hala intuygulama tanımlı davranışı çağırıp çağırmayacağını kontrol etmeden adresine çeviriyorum. 2050'deki bazı varsayımsal sistemlerde, kim bilir ne yapabilirdi. Diyelim ki bundan kaçınmak istiyorum.

Soru: "Üçüncü denemem" neye benzemeli?

Özetlemek gerekirse, şunu yapmak istiyorum:

  • İşaretsiz int'ten imzalı int'e çevir
  • Değer modunu UINT_MAX + 1 koruyun
  • Yalnızca standart zorunlu davranışı çağırın
  • Optimize edici derleyici ile tipik bir iki tamamlayıcı makinede işlemsiz olarak derleyin

[Güncelleme]

Bunun neden önemsiz bir soru olmadığını göstermek için bir örnek vereyim.

Aşağıdaki özelliklere sahip varsayımsal bir C ++ uygulamasını düşünün:

  • sizeof(int) eşittir 4
  • sizeof(unsigned) eşittir 4
  • INT_MAX eşittir 32767
  • INT_MINeşittir -2 32 + 32768
  • UINT_MAX2 eşit 32 - 1
  • Aritmetik açık int, modulo 2 32'dir (aralığa INT_MINkadar INT_MAX)
  • std::numeric_limits<int>::is_modulo doğru
  • İşaretsiz nolarak int türüne çevrim 0 <= n <= 32767 değerini korur ve aksi takdirde sıfır verir

Bu varsayımsal uygulamada, her bir intdeğere uyumlu tam olarak bir değer (mod UINT_MAX + 1) vardır unsigned. Yani sorum iyi tanımlanmış olacak.

Bu varsayımsal C ++ uygulamasının C ++ 98, C ++ 03 ve C ++ 11 spesifikasyonlarına tamamen uygun olduğunu iddia ediyorum. Her kelimesini ezberlemediğimi kabul ediyorum ... Ama ilgili bölümleri dikkatlice okuduğuma inanıyorum. Öyleyse cevabınızı kabul etmemi istiyorsanız, ya (a) bu varsayımsal uygulamayı dışlayan bir özelliğe atıfta bulunmalısınız ya da (b) doğru şekilde ele almalısınız.

Gerçekte, doğru bir yanıt , standardın izin verdiği her varsayımsal uygulamayı ele almalıdır . Tanım gereği, "yalnızca standart yönetimli davranışı çağırmak" budur.

Bu arada, bunun std::numeric_limits<int>::is_modulobirçok nedenden dolayı burada tamamen yararsız olduğunu unutmayın . Birincisi, truebüyük işaretsiz değerler için imzasız-imzalı yayınlar çalışmasa bile olabilir. Bir diğeri için, truearitmetik basitçe tüm tamsayı aralığını modulo ise, kişinin tümleyen veya işaret büyüklüğü sistemlerinde bile olabilir . Ve bunun gibi. Cevabınız şuna bağlıysais_modulo yanlıştır.

[Güncelleme 2]

HVD cevabı tamsayılar için My varsayımsal C ++ uygulama olduğunu: bana bir şey öğretti değil imzalı tamsayı temsil konusunda çok özeldir Modern C. C99 ve C11 standartlarına göre izin verilen; gerçekte, sadece iki-tümleme, bir-tümleme ve işaret büyüklüğüne izin verirler (bölüm 6.2.6.2 paragraf (2);).

Ancak C ++, C değildir. Görünüşe göre, sorumun tam merkezinde bu gerçek yatıyor.

Orijinal C ++ 98 standardı, (bölüm 3.1.2.5) şunu söyleyen çok daha eski C89'a dayanıyordu:

İşaretli tam sayı türlerinin her biri için, aynı miktarda depolamayı (işaret bilgileri dahil) kullanan ve aynı hizalama gereksinimlerine sahip karşılık gelen (ancak farklı) işaretsiz bir tamsayı türü (işaretsiz anahtar sözcüğüyle belirtilir) vardır. İşaretli bir tamsayı türünün negatif olmayan değerler aralığı, karşılık gelen işaretsiz tamsayı türünün bir alt aralığıdır ve her türdeki aynı değerin gösterimi aynıdır.

C89, yalnızca bir işaret biti olması veya yalnızca iki-tamamlayıcı / bir-tamamlayıcı / işaret büyüklüğüne izin verilmesi hakkında hiçbir şey söylemiyor.

C ++ 98 standardı bu dili neredeyse aynen benimsemiştir (bölüm 3.9.1 paragraf (3)):

İşaretli tam sayı türlerinin her biri için, karşılık gelen (ancak farklı) işaretsiz bir tam sayı türü vardır : " unsigned char", " unsigned short int", " unsigned int" ve " unsigned long int", her biri aynı miktarda depolama alanı kaplar ve aynı hizalama gereksinimlerine sahiptir (3.9 ) karşılık gelen işaretli tamsayı türü olarak; yani, her işaretli tamsayı türü, karşılık gelen işaretsiz tamsayı türü ile aynı nesne temsiline sahiptir . İşaretli bir tamsayı türünün negatif olmayan değerler aralığı, karşılık gelen işaretsiz tamsayı türünün bir alt aralığıdır ve karşılık gelen her bir işaretli / işaretsiz türün değer temsili aynı olacaktır.

C ++ 03 standardı, C ++ 11 gibi temelde aynı dili kullanır.

Anlayabildiğim kadarıyla hiçbir standart C ++ özelliği, işaretli tamsayı temsillerini herhangi bir C spesifikasyonu ile sınırlamaz. Ve tek bir işaret parçasını ya da herhangi bir şeyi zorunlu kılan hiçbir şey yoktur. Tek söylediği negatif olmayan işaretli tam sayıların karşılık gelen işaretsizlerin bir alt aralığı olması gerektiğidir.

Bu yüzden, yine INT_MAX = 32767 ile INT_MIN = -2 32 + 32768'e izin verildiğini iddia ediyorum . Cevabınız aksini varsayıyorsa, beni yanlış olduğunu kanıtlayan bir C ++ standardından alıntı yapmadığınız sürece yanlıştır.


@SteveJessop: Aslında bu durumda tam olarak ne istediğimi belirttim: "İmzasız int ile UINT_MAX + 1 uyumlu bir int uyumlu modulo yoksa, diyelim ki bir istisna atmak istiyorum." Yani, var olması koşuluyla "doğru" imzalı int istiyorum. Eğer yoksa - örneğin doldurma bitleri veya bir-tamamlayıcı temsiller durumunda olduğu gibi - bunu tespit etmek ve dökümün o belirli çağrısı için işlemek istiyorum.
Nemo

üzgünüm, bunu nasıl kaçırdığımdan emin değilim.
Steve Jessop

Btw, varsayımsal karmaşık uygulamanızda intonu temsil etmek için en az 33 bit gerektiğini düşünüyorum. Bunun sadece bir dipnot olduğunu biliyorum, bu yüzden normatif olmadığını iddia edebilirsiniz, ancak bence C ++ 11'deki 49 numaralı dipnotun doğru olması amaçlanmıştır (çünkü standartta kullanılan bir terimin tanımıdır) ve çelişmez normatif metinde açıkça belirtilen herhangi bir şey. Bu nedenle, tüm negatif değerler, en yüksek bitin ayarlandığı bir bit örüntüsü ile temsil edilmelidir ve bu nedenle 2^32 - 32768, bunları 32 bit olarak sıkıştıramazsınız . Sizin argümanınız herhangi bir şekilde boyutuna bağlı değil int.
Steve Jessop

Ve hvd'nin cevabındaki düzenlemelerinizle ilgili olarak, 49. notu yanlış yorumladığınızı düşünüyorum. İşaret büyüklüğünün yasak olduğunu söylüyorsunuz, ama değil. Bunu şu şekilde okudunuz: "ardışık bitler tarafından temsil edilen değerler toplamadır, 1 ile başlar ve (belki en yüksek konuma sahip bit hariç, 2'nin ardışık integral gücü ile çarpılır)". Bunun okunması gerektiğine inanıyorum, "ardışık bitlerle temsil edilen değerler (toplamadır, 1 ile başlar ve 2'nin ardışık integral kuvveti ile çarpılır), belki de en yüksek konuma sahip bit hariç". Yani, yüksek bit ayarlanmışsa tüm bahisler kapalıdır.
Steve Jessop

@SteveJessop: Yorumunuz doğru olabilir. Eğer öyleyse, bu benim varsayımımı dışlıyor ... Ama aynı zamanda gerçekten çok sayıda olasılık sunuyor ve bu soruyu cevaplamayı son derece zor hale getiriyor. Bu aslında teknik özellikteki bir hataya benziyor bana. (Görünüşe göre, C komitesi öyle düşündü ve C99'da kapsamlı bir şekilde düzeltti. C ++ 11'in neden bu yaklaşımları benimsemediğini merak ediyorum?)
Nemo

Yanıtlar:


70

User71404'ün cevabını genişleterek:

int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}

Eğer x >= INT_MIN(terfi kurallarını aklınızda bulundurun, INT_MINdönüştürülürse unsigned), o zamanx - INT_MIN <= INT_MAX bu herhangi bir taşma olmayacak.

Bu açık değilse x >= -4u, "Eğer öyleyse " iddiasına bir bakın ve bunun en azından -INT_MIN - 1'in matematiksel değerine eşit olacağını x + 4 <= 3aklınızda bulundurun INT_MAX.

En yaygın sistemlerde, !(x <= INT_MAX)ima edildiği yerde x >= INT_MIN, optimize edicinin ikinci kontrolü kaldırabilmesi (ve benim sistemimde), iki returnifadenin aynı kodda derlenebileceğini belirlemesi ve ilk kontrolü de kaldırması gerekir. Oluşturulan montaj listesi:

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

Sorunuzdaki varsayımsal uygulama:

  • INT_MAX, 32767'ye eşittir
  • INT_MIN, -2 32 + 32768'e eşittir

mümkün değildir, bu nedenle özel bir değerlendirmeye gerek yoktur. veya a INT_MINeşit olacaktır . Bu, bitlerin değer bitleri olmasını, bir bitin işaret biti olmasını gerektiren ve yalnızca tek bir tuzak gösterimine izin veren (dolgu bitleri nedeniyle geçersiz olan gösterimler dahil değil ), C'nin tamsayı türleri temsilinden (6.2.6.2 ) kaynaklanır, başka bir deyişle, aksi takdirde negatif sıfırı temsil edecek olan / . C ++, C'nin izin verdiğinin ötesinde herhangi bir tamsayı gösterimine izin vermez.-INT_MAX-INT_MAX - 1n-INT_MAX - 1

Güncelleme : Görünüşe göre Microsoft'un derleyicisi bunu fark etmiyor x > 10ve x >= 11aynı şeyi test ediyor. Yalnızca x >= INT_MINile değiştirilirse istenen kodu üretir, bu kodu x > INT_MIN - 1uolumsuzlama olarak algılayabilirx <= INT_MAX (bu platformda).

[Soru sorucudan (Nemo) güncelleme, aşağıdaki tartışmamızı detaylandırıyor]

Şimdi bu cevabın her durumda işe yaradığına inanıyorum, ancak karmaşık nedenlerden dolayı. Muhtemelen bu çözüme ödül vereceğim, ancak herhangi birinin umursaması durumunda tüm kanlı ayrıntıları yakalamak istiyorum.

C ++ 11, bölüm 18.3.3 ile başlayalım:

Tablo 31 başlığı açıklamaktadır <climits>.

...

İçerikler, Standart C kitaplığı başlığıyla aynıdır <limits.h>.

Burada "Standart C", spesifikasyonu işaretli tamsayıların temsilini ciddi şekilde kısıtlayan C99 anlamına gelir. Tıpkı işaretsiz tamsayılar gibidirler, ancak bir biti "işarete" ve sıfır veya daha fazla biti "doldurmaya" ayrılmışlardır. Dolgu bitleri, tamsayının değerine katkıda bulunmaz ve işaret biti yalnızca iki-tamamlayıcı, bir-tamamlayıcı veya işaret-büyüklüğü olarak katkıda bulunur.

C ++ 11, <climits> makroları C99'dan , INT_MIN, -INT_MAX veya -INT_MAX-1'dir ve hvd kodunun çalışması garantilidir. (Dolgu nedeniyle, INT_MAX'ın UINT_MAX / 2'den çok daha az olabileceğini unutmayın ... Ancak işaretli-> işaretsiz yayınların çalışma şekli sayesinde, bu yanıt bu cezayı ele alıyor.)

C ++ 03 / C ++ 98 daha zordur. <climits>"Standart C" den devralmak için aynı ifadeyi kullanır , ancak artık "Standart C", C89 / C90 anlamına gelir.

Bunların tümü - C ++ 98, C ++ 03, C89 / C90 - sorumda verdiğim ifadelere sahip, ancak bunu da içeriyor (C ++ 03 bölüm 3.9.1 paragraf 7):

İntegral türlerinin gösterimleri, değerleri saf bir ikili sayı sistemi kullanarak tanımlayacaktır. (44) [ Örnek : Bu Uluslararası Standart, integral türleri için 2'nin tümleyen, 1'in tümleyen ve işaretli büyüklük gösterimlerine izin verir.]

Dipnot (44) "saf ikili sayı sistemini" tanımlar:

0 ve 1 ikili rakamlarını kullanan tamsayılar için bir konumsal temsil, burada ardışık bitler tarafından temsil edilen değerler toplamadır, 1 ile başlar ve muhtemelen en yüksek konuma sahip bit haricinde 2'nin ardışık integral gücü ile çarpılır.

Bu üslupla ilgili ilginç olan şey, kendisiyle çelişmesidir, çünkü "saf ikili sayı sistemi" tanımı bir işaret / büyüklük temsiline izin vermez! Yüksek bitin, örneğin -2 n-1 (iki tamamlayıcı) veya - (2 n-1 -1) (birler tamamlayıcı) değerine sahip olmasına izin verir . Ancak işaret / büyüklükle sonuçlanan yüksek bit için bir değer yoktur.

Her neyse, benim "varsayımsal uygulamam" bu tanıma göre "saf ikili" olarak nitelendirilmiyor, bu yüzden göz ardı ediliyor.

Bununla birlikte, yüksek bitin özel olduğu gerçeği, herhangi bir değere katkıda bulunduğunu hayal edebileceğimiz anlamına gelir: Küçük bir pozitif değer, çok büyük bir pozitif değer, küçük bir negatif değer veya çok büyük bir negatif değer. (İşaret biti katkıda bulunabiliyorsa - (2 n-1 -1), neden olmasın - (2 n-1 -2)? Vb.)

Öyleyse, "işaret" bitine tuhaf bir değer atayan işaretli bir tam sayı gösterimi hayal edelim.

İşaret biti için küçük bir pozitif değer, için pozitif bir aralıkla sonuçlanır int(muhtemelen kadar büyüktür unsigned) ve hvd'nin kodu bunu idare eder.

İşaret biti için çok büyük bir pozitif değer , yasaklanmış olan intmaksimum değerden daha büyük olmasına unsignedneden olur.

İşaret biti için çok büyük bir negatif değer, intbitişik olmayan bir değer aralığını temsil eder ve spesifikasyondaki diğer ifadeler bunu kurallara uydurur.

Son olarak, küçük bir negatif miktara katkıda bulunan bir işaret bitine ne dersiniz? "İşaret bitinde" 1, int değerine -37 gibi bir katkıda bulunabilir miyiz? O halde INT_MAX (diyelim) 2 31 -1 ve INT_MIN -37 olur?

Bu, bazı sayıların iki gösterime sahip olmasına neden olur ... Ama birler-tamamlayıcı, sıfıra iki temsil verir ve buna "Örnek" e göre izin verilir. Spesifikasyon hiçbir yerde sıfırın iki temsili olabilecek tek tam sayı olduğunu söylemez . Bu yüzden bu yeni varsayıma şartname tarafından izin verildiğini düşünüyorum.

Gerçekte, -1'den aşağıya kadar herhangi bir negatif değere -INT_MAX-1, "işaret biti" için bir değer olarak izin verilebilir, ancak daha küçük hiçbir değer yoktur (aralık bitişik olmayacağı için). Başka bir deyişle, -1 ile INT_MINarası herhangi bir şey olabilir -INT_MAX-1.

Şimdi, tahmin et ne oldu? Uygulama tanımlı davranıştan kaçınmak için hvd kodundaki ikinci çevrim için, sadece x - (unsigned)INT_MINküçük veya eşit olmalıdır INT_MAX. INT_MINEn azından gösterdik -INT_MAX-1. Açıkçası, xen fazla UINT_MAX. Negatif bir sayıyı işaretsiz olarak çevirmek, eklemekle aynıdır UINT_MAX+1. Hepsini bir araya getirmek:

x - (unsigned)INT_MIN <= INT_MAX

ancak ve ancak

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

Bu sonuncusu az önce gösterdiğimiz şeydir, bu yüzden bu sapkın durumda bile, kod gerçekten çalışıyor.

Bu, tüm olasılıkları tüketir ve bu son derece akademik egzersizi sonlandırır.

Alt satır: C89 / C90'da C ++ 98 / C ++ 03 tarafından miras alınan işaretli tamsayılar için ciddi şekilde eksik belirtilmiş bazı davranışlar vardır. C99'da düzeltilmiştir ve C ++ 11 dolaylı olarak düzeltmeyi <limits.h>C99'dan dahil ederek devralır . Ama C ++ 11 bile kendisiyle çelişen "saf ikili temsil" ifadesini koruyor ...


Soru güncellendi. Bu yanıtı (şimdilik) diğerlerini caydırmak için aşağı oyluyorum ... Daha sonra olumsuz oyu geri alacağım çünkü yanıt ilginç. (C için doğru, ancak C ++ için yanlış. Sanırım.)
Nemo

@Nemo C standardı bu durumda C ++ için geçerlidir; en azından, içindeki değerler <limits.h>C ++ standardında, C standardında olduğu gibi aynı anlama sahip olarak tanımlanır, bu nedenle C'nin tüm gereksinimleri INT_MINve INT_MAXC ++ içinde miras alınır. C ++ 03'ün C90'a atıfta bulunduğu ve C90'ın izin verilen tamsayı temsilleri konusunda belirsiz olduğu konusunda haklısınız, ancak C99 değişikliği (en azından <limits.h>C ++ 11 aracılığıyla , umarız ki daha basit bir şekilde miras alınır ) bu üçü mevcut uygulamayı kodlayan biriydi: başka hiçbir uygulama mevcut değildi.

INT_MINVb. Nin anlamının C'den miras kaldığını kabul ediyorum . Ancak bu, değerlerin olduğu anlamına gelmez . (Aslında, her uygulama farklı olduğu için nasıl olabilirler?) INT_MIN1 / 1'lik çıkarımınız, -INT_MAXherhangi bir C ++ spesifikasyonunda görünmeyen ifadelere bağlıdır. Dolayısıyla, C ++ makroların anlamsal anlamını devralırken, özellik çıkarımınızı destekleyen ifadeleri sağlamaz (veya devralmaz). Bu, C ++ spesifikasyonunda tam uyumlu, verimli bir imzasız-imzalı atamayı engelleyen bir gözetim gibi görünmektedir.
Nemo

@Nemo C ++ sonra böyle bir uygulama üstünde, ben iddia, diğer temsillerini izin vermesidir (belki doğru) iddia ederse INT_MIN değildir tip minimal temsil edilebilir değer olması gerekli inttip yapmazsa, söz konusu çünkü uzak C'ye kadar, gereksinimlerini karşılamak int, C standart muhtemelen herhangi bir şekilde bu uygulama ve C örtemez ++ standardı "C standart ne diyor" dışında hiçbirini tanımını sağlamaz. Daha basit bir açıklama olup olmadığını kontrol edeceğim.

7
Bu muhteşem. O sırada bu soruyu nasıl kaçırdığımı bilmiyorum.
Orbit'te Hafiflik Yarışları

17

Bu kod yalnızca spesifikasyon tarafından zorunlu kılınan davranışa dayanır, bu nedenle (a) gereksinimi kolayca karşılanır:

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

(B) gereği o kadar kolay değil. Bu, gcc 4.6.3 (-Os, -O2, -O3) ve clang 3.0 (-Os, -O, -O2, -O3) ile işlemsiz olarak derlenir. Intel 12.1.0 bunu optimize etmeyi reddediyor. Ve Görsel C hakkında hiçbir bilgim yok.


1
Tamam, bu harika. Keşke ikramiyeyi 80:20 bölebilseydim ... Derleyicinin mantığının gittiğinden şüpheleniyorum: Döngü sona resultermezse taşar; tamsayı taşması tanımsız; bu nedenle döngü sona erer; bu nedenle i == nfesih sırasında; bu nedenle resulteşittir n. Hala hvd'nin cevabını tercih etmeliyim (daha az akıllı derleyicilerde patolojik olmayan davranış için), ancak bu daha fazla oyu hak ediyor.
Nemo

1
İşaretsiz modulo olarak tanımlanır. Döngünün sona ermesi de garanti edilir n, çünkü bazı işaretsiz bir değerdir ve isonunda her işaretsiz değere ulaşması gerekir.
idupree

7

Orijinal yanıt sorunu yalnızca unsigned=> için çözdü int. Ya "bazı işaretsiz tipler" genel problemini karşılık gelen imzalı tipe çözmek istiyorsak? Dahası, orijinal cevap standardın bölümlerinden alıntı yapmakta ve bazı köşe vakaları analiz etmekte mükemmeldi, ancak neden işe yaradığına dair bir fikir edinmeme gerçekten yardımcı olmadı, bu nedenle bu cevap güçlü bir kavramsal temel oluşturmaya çalışacak. Bu cevap, "neden" i açıklamaya yardımcı olacak ve kodu basitleştirmek için modern C ++ özelliklerini kullanacaktır.

C ++ 20 yanıtı

Sorun, P0907 ile çarpıcı bir şekilde basitleştirildi : İmzalı Tamsayılar İkinin Tamamlayıcısıdır ve C ++ 20 standardında oylanan son ifade P1236'dır . Şimdi, cevap olabildiğince basit:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

Bu kadar. Bir static_cast(veya C tarzı oyuncu) sonunda bu soru için ihtiyaç duyduğunuz şeyi ve birçok programcının her zaman yaptığını düşündüğü şeyi yapması garantidir.

C ++ 17 yanıtı

C ++ 17'de işler çok daha karmaşıktır. Üç olası tamsayı temsiliyle uğraşmalıyız (ikinin tümleyenleri, birlerin tümleyeni ve işaret-büyüklüğü). Olası değerlerin aralığını kontrol ettiğimiz için ikinin tamamlayıcısı olması gerektiğini bildiğimiz durumda bile, işaretli tamsayı aralığının dışındaki bir değerin bu işaretli tam sayıya dönüştürülmesi hala bize uygulama tanımlı bir sonuç verir. Diğer cevaplarda gördüğümüz gibi hileler kullanmalıyız.

İlk olarak, sorunun genel olarak nasıl çözüleceğine ilişkin kod:

template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}

Bu, kabul edilen yanıttan birkaç daha fazla yayın içerir ve bu, derleyicinizden imzalı / işaretsiz uyuşmazlık uyarıları olmamasını sağlamak ve tamsayı yükseltme kurallarını uygun şekilde işlemek içindir.

Öncelikle ikinin tamamlayıcısı olmayan sistemler için özel bir durumumuz var (ve bu nedenle mümkün olan maksimum değeri özel olarak ele almalıyız çünkü eşlenecek hiçbir şeyi yok). Bundan sonra gerçek algoritmaya geçiyoruz.

İkinci üst düzey koşul basittir: Değerin maksimum değerden küçük veya ona eşit olduğunu biliyoruz, bu nedenle sonuç türüne uyuyor. Üçüncü koşul, yorumlarda bile biraz daha karmaşıktır, bu nedenle bazı örnekler muhtemelen her bir ifadenin neden gerekli olduğunu anlamaya yardımcı olacaktır.

Kavramsal temel: sayı doğrusu

Birincisi, bu windowkavram nedir? Aşağıdaki sayı doğrusunu düşünün:

   |   signed   |
<.........................>
          |  unsigned  |

İkinin tamamlayıcı tamsayıları için, her iki türden de ulaşılabilen sayı doğrusu alt kümesini üç eşit boyutlu kategoriye bölebileceğiniz ortaya çıktı:

- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>

Temsil dikkate alınarak bu kolayca kanıtlanabilir. İşaretsiz bir tam sayı olarak başlar 0ve tüm bit kullanımları değer işaret biti dışında tam bit tümü için aynıdır 2. imzalı tamsayı kuvvetleri cinsinden değerini artırmak için -(2^position)yerine 2^position. Bu, tüm n - 1bitler için aynı değerleri temsil ettikleri anlamına gelir . Daha sonra, işaretsiz tamsayıların bir tane daha normal biti vardır, bu da toplam değer sayısını iki katına çıkarır (başka bir deyişle, o bit setinde setsiz olduğu kadar çok değer vardır). Aynı mantık, işaretli tamsayılar için de geçerlidir, tek fark o bit kümesine sahip tüm değerlerin negatif olmasıdır.

Diğer iki yasal tamsayı temsili, birlerin tamamlayıcı ve işaret-büyüklüğü, ikisinin tamamlayıcı tamsayılarla biri dışında tüm değerlere sahiptir: en negatif değer. C ++ , bit gösterimi açısından değil, gösterilebilir değerlerin aralığı açısından reinterpret_cast(ve C ++ 20 std::bit_cast) dışında tamsayı türleri hakkındaki her şeyi tanımlar . Bu, tuzak temsilini yaratmaya çalışmadığımız sürece analizimizin bu üç temsilin her biri için geçerli olacağı anlamına gelir. Bu eksik değere eşlenecek işaretsiz değer, oldukça talihsiz bir değerdir: işaretsiz değerlerin tam ortasındaki değer. Neyse ki, ilk koşulumuz böyle bir temsilin var olup olmadığını kontrol eder (derleme zamanında) ve daha sonra bunu bir çalışma zamanı kontrolü ile özel olarak ele alır.

İlk koşul, =bölüm içinde olduğumuz durumu ele alır , bu, birindeki değerlerin diğerinde değişmeden temsil edilebildiği üst üste binen bölgede olduğumuz anlamına gelir. Koddaki shift_by_windowişlev, tüm değerleri bu segmentlerin her birinin boyutuna göre aşağı taşır (aritmetik taşma sorunlarını önlemek için maksimum değeri çıkarmalı ve ardından 1 çıkarmalıyız). O bölgenin dışındaysak ( bölgedeyiz), böylece yine benzersiz bir haritaya sahip oluruz.+ bölgedeysek), bir pencere boyutu aşağı atlamamız gerekir. Bu bizi örtüşen aralığa koyar, bu da değerde bir değişiklik olmadığı için güvenli bir şekilde işaretsizden işaretliye geçebileceğimiz anlamına gelir. Ancak, imzasız iki değeri her bir işaretli değere eşlediğimiz için henüz bitirmedik. Bu nedenle, bir sonraki pencereye geçmemiz gerekiyor (-

Şimdi, bu bize UINT_MAX + 1soruda talep edildiği gibi sonuç uyumlu mod veriyor mu? değer gösteriminde bit sayısı nerede olduğu UINT_MAX + 1ile eşdeğerdir . Pencere boyutumuz için kullandığımız değer şuna eşittir (değerler dizisindeki son indeks boyuttan bir küçüktür). Bu değeri iki kez çıkarırız , yani eşit olanı çıkarırız . Toplama ve çıkarma aritmetik modda işlem gerektirmez , bu nedenle orijinal değer modunu etkilemedik .2^nn2^(n - 1)2 * 2^(n - 1)2^nxx2^n

Tamsayı promosyonlarını uygun şekilde işleme

Bu genel bir fonksiyonu ve olmadığından sadece intve unsignedbiz de ayrılmaz tanıtım kurallarına endişe kendimizi zorundayız. Muhtemelen iki ilginç durum vardır: biri shortdaha küçük, intdiğeri shortaynı büyüklükte int.

Örnek: shortdaha küçükint

Daha shortküçükse int(modern platformlarda yaygındır) o zaman bunun unsigned shortbir'e sığabileceğini de biliyoruz int, bu da üzerindeki herhangi bir işlemin gerçekten gerçekleşeceği anlamına gelir int, bu nedenle bundan kaçınmak için açıkça yükseltilmiş türe atarız. Nihai ifademiz oldukça soyuttur ve gerçek değerleri yerine koyarsak anlaşılması daha kolay hale gelir. İlk ilginç durumumuz için, genellik kaybı olmaksızın bir 16 bit shortve bir 17 bit düşünelim int(yeni kurallara göre hala izin verilmektedir ve bu iki tam sayı türünden en az birinin bazı dolgu bitlerine sahip olduğu anlamına gelir. ):

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

Mümkün olan en büyük 16 bitlik işaretsiz değeri çözme

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

Basitleştirir

return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

Basitleştirir

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

Basitleştirir

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

Basitleştirir

return int16_t(-1);

Mümkün olan en büyük imzasızları koyarız ve geri döneriz -1, başarı!

Örnek: shortaynı boyuttaint

Eğer shortaynı boyutta int(modern platformlarda nadir), ayrılmaz promosyon kural biraz farklıdır. Bu durumda, shortyükseltir intve unsigned shortyükseltir unsigned. Neyse ki, her sonucu açık bir şekilde hesaplamayı yapmak istediğimiz türe atıyoruz, bu nedenle sorunlu promosyonlar yaşamıyoruz. Genelliği kaybetmeden 16 bit shortve 16 bit düşünelim int:

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

Mümkün olan en büyük 16 bitlik işaretsiz değeri çözme

auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

Basitleştirir

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

Basitleştirir

return int16_t(-1);

Mümkün olan en büyük imzasızları koyarız ve geri döneriz -1, başarı!

Ya orijinal soruda olduğu gibi, sadece önemsiyor intve unsigneduyarıları önemsemiyorsam?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

Canlı görün

https://godbolt.org/z/74hY81

İşte biz bu çınlama, gcc ve icc için herhangi bir kod yarattığını görebilir castve cast_to_signed_integer_basicen -O2ve -O3ve MSVC hiçbir kod oluşturur /O2çözüm en uygunudur yüzden.


3

Derleyiciye ne yapmak istediğinizi açıkça söyleyebilirsiniz:

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

gcc 4.7.2For x86_64-linux( g++ -O -S test.cpp) ile derler

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAXbir tür ifadesidir unsigned intve bu, sizin static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1)o türün tamamını oluşturur. Yine de bunu düzeltmek mümkün olmalı ve ben yine de aynı şekilde derlenmesini bekliyorum.

2

Eğer xbizim girişi olduğunu ...

Eğer x > INT_MAX, bir sabiti bulmak istiyorum kböyle 0< x - k*INT_MAX< INT_MAX.

Bu çok kolay - unsigned int k = x / INT_MAX;. O halde bırakunsigned int x2 = x - k*INT_MAX;

Artık yayın yapabiliyorsa x2için intgüvenli. İzin Vermekint x3 = static_cast<int>(x2);

Şimdi böyle bir şey çıkarmak isteyen UINT_MAX - k * INT_MAX + 1dan x3eğer k > 0.

Şimdi, 2s tamamlayıcı bir sistemde x > INT_MAX, bu işe yaradığı sürece :

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Not UINT_MAX+1C ++ sıfırdır int dönüştürme noop olduğunu garanti, ve çıkarılırk*INT_MAX sonra "aynı değere" ile geri ilave edildi. Bu nedenle, kabul edilebilir bir optimizasyon uzmanı tüm bu saçmalıkları silebilir!

Bu sorunu bırakır x > INT_MAXya da bırakmaz. Peki, biri olan x > INT_MAXdiğeri olmayan 2 şube oluşturuyoruz . Olmayan, derleyicinin bir noop için optimize ettiği bir strait cast yapar. İyileştirici tamamlandıktan sonra ... olan kişi noop yapar. Akıllı optimize edici, her iki dalı da aynı şeyi gerçekleştirir ve dalı bırakır.

Sorunlar: UINT_MAXgöre gerçekten büyükse INT_MAX, yukarıdakiler işe yaramayabilir. Bunu k*INT_MAX <= UINT_MAX+1dolaylı olarak varsayıyorum .

Muhtemelen buna bazı numaralandırmalarla saldırabiliriz:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

2s tamamlayıcı sistemde 2'ye ve 1'e çalışan hangisi (bu matematiğin çalışması için garantili miyiz? Bu zor ...) ve 2s olmayan tamamlayıcı sistemlerde kolayca optimize eden bunlara dayalı mantık yapıyor ...

Bu aynı zamanda istisna durumunu da açar. Yalnızca UINT_MAX (INT_MIN-INT_MAX) 'dan çok daha büyükse mümkündür, böylece istisna kodunuzu bir if bloğunun içine tam olarak bu soruyu sorabilirsiniz ve bu sizi geleneksel bir sistemde yavaşlatmaz.

Bununla doğru bir şekilde başa çıkmak için bu derleme zamanı sabitlerini nasıl inşa edeceğimi tam olarak bilmiyorum.


UINT_MAXINT_MAXSpesifikasyon, her pozitif işaretli int'in işaretsiz bir int olarak temsil edilebileceğini garanti ettiğinden, göre küçük olamaz . Ancak UINT_MAX+1her sistemde sıfırdır; işaretsiz aritmetik her zaman modulodur UINT_MAX+1. Yine de burada uygulanabilir bir yaklaşımın çekirdeği olabilir ...
Nemo

@Nemo Sadece bu konuyu takip ederek, potansiyel olarak açık olan sorumu affedin: UINT_MAX+1İfadeniz '03 -spec'te kurulan " her sistemde sıfır mı?" Eğer öyleyse, altında
bakmam

@WhozCraig: Bölüm 3.9.1 paragraf 4: "İşaretsiz olarak beyan edilen işaretsiz tamsayılar, aritmetik modülo 2 ^ n yasalarına uyacaktır; burada n, belirli bir tam sayı boyutunun değer gösterimindeki bit sayısıdır", bir dipnotla birlikte "Bu, işaretsiz aritmetiğin taşmaması anlamına gelir, çünkü sonuçta ortaya çıkan işaretsiz tamsayı türü tarafından temsil edilemeyen bir sonuç, sonuçta ortaya çıkan işaretsiz tamsayı türü tarafından temsil edilebilen en büyük değerden bir büyük olan sayıya indirgenir." Temelde işaretsiz, istediğiniz / beklediğiniz şekilde çalışması için belirtilir.
Nemo

@Nemo Teşekkürler. çok müteşekkirim.
WhozCraig

1

std::numeric_limits<int>::is_modulobir derleme zamanı sabitidir. böylece şablon uzmanlığı için kullanabilirsiniz. sorun çözüldü, en azından derleyici satır içi ile birlikte oynarsa.

#include <limits>
#include <stdexcept>
#include <string>

#ifdef TESTING_SF
    bool const testing_sf = true;
#else
    bool const testing_sf = false;
#endif

// C++ "extensions"
namespace cppx {
    using std::runtime_error;
    using std::string;

    inline bool hopefully( bool const c ) { return c; }
    inline bool throw_x( string const& s ) { throw runtime_error( s ); }

}  // namespace cppx

// C++ "portability perversions"
namespace cppp {
    using cppx::hopefully;
    using cppx::throw_x;
    using std::numeric_limits;

    namespace detail {
        template< bool isTwosComplement >
        int signed_from( unsigned const n )
        {
            if( n <= unsigned( numeric_limits<int>::max() ) )
            {
                return static_cast<int>( n );
            }

            unsigned const u_max = unsigned( -1 );
            unsigned const u_half = u_max/2 + 1;

            if( n == u_half )
            {
                throw_x( "signed_from: unsupported value (negative max)" );
            }

            int const i_quarter = static_cast<int>( u_half/2 );
            int const int_n1 = static_cast<int>( n - u_half );
            int const int_n2 = int_n1 - i_quarter;
            int const int_n3 = int_n2 - i_quarter;

            hopefully( n == static_cast<unsigned>( int_n3 ) )
                || throw_x( "signed_from: range error" );

            return int_n3;
        }

        template<>
        inline int signed_from<true>( unsigned const n )
        {
            return static_cast<int>( n );
        }
    }    // namespace detail

    inline int signed_from( unsigned const n )
    {
        bool const is_modulo = numeric_limits< int >::is_modulo;
        return detail::signed_from< is_modulo && !testing_sf >( n );
    }
}    // namespace cppp

#include <iostream>
using namespace std;
int main()
{
    int const x = cppp::signed_from( -42u );
    wcout << x << endl;
}


DÜZENLEME : Modüler olmayan int makinelerde olası tuzaktan kaçınmak için kod düzeltildi (yalnızca birinin, yani Unisys Clearpath'ın arkaik olarak yapılandırılmış sürümleri olduğu bilinmektedir). Basitlik açısından bu, böyle bir makinede (yani Clearpath'ta) n değer bitlerinin sayısı olduğu -2 n -1 değerini desteklemeyerek yapılır . pratikte bu değer makine tarafından da desteklenmeyecektir (yani işaret ve büyüklük veya 1'in tamamlayıcı gösterimi ile).int


1

İnt türünün en az iki bayt olduğunu düşünüyorum, bu nedenle INT_MIN ve INT_MAX farklı platformlarda değişebilir.

Temel türler

≤climits≥ başlığı


6809 için varsayılan olarak "-mint8" ile yapılandırılmış bir derleyici kullanmak için lanetlendim, burada int 8 bittir :-( (bu Vectrex için geliştirme ortamıdır) uzun 2 bayt, uzun uzun 4 bayttır ve Kısa ne olduğu hakkında hiçbir fikrim yok ...
Graham Toal

1

Param memcpy kullanıyor. İyi bir derleyici, onu optimize etmeyi bilir:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

Benim için (Xcode 8.3.2, Apple LLVM 8.1, -O3) şunları üretir:

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

1
İşaretsizin ikili gösteriminin standart tarafından imzalı temsil ile eşleşeceği garanti edilmediğinden , bu soruya cevap vermez .
TLW
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.