985 Stimmen

Was ist die strenge Aliasing-Regel?

Bei der Frage nach allgemeines undefiniertes Verhalten in C wird manchmal auf die strenge Aliasing-Regel verwiesen.
Wovon reden sie?

8 Stimmen

Vielleicht interessieren Sie sich auch für einen Artikel, den ich kürzlich geschrieben habe Was ist die Strict Aliasing Rule und warum ist sie wichtig? . Es deckt eine Menge Material ab, das hier nicht behandelt wird, oder bietet in einigen Bereichen einen moderneren Ansatz.

284voto

Niall Punkte 4743

Die beste Erklärung, die ich gefunden habe, stammt von Mike Acton, Strict Aliasing verstehen . Es konzentriert sich ein wenig auf die PS3-Entwicklung, aber das ist im Grunde nur GCC.

Aus dem Artikel:

"Striktes Aliasing ist eine Annahme des C- (oder C++-) Compilers, dass dereferenzierende Zeiger auf Objekte unterschiedlichen Typs niemals auf dieselbe Speicherstelle verweisen (d.h. sich gegenseitig aliasieren)."

Wenn Sie also eine int* die auf einen Speicher verweist, der eine int und dann zeigen Sie eine float* zu diesem Speicher und verwenden ihn als float brechen Sie die Regel. Wenn Ihr Code dies nicht beachtet, wird der Optimierer des Compilers Ihren Code höchstwahrscheinlich zerstören.

Die Ausnahme von der Regel ist eine char* der auf einen beliebigen Typ zeigen darf.

8 Stimmen

Also, was ist der kanonische Weg, um legal den gleichen Speicher mit Variablen von 2 verschiedenen Typen zu verwenden? oder kopiert jeder einfach?

5 Stimmen

Die Seite von Mike Acton ist fehlerhaft. Zumindest der Teil "Casting durch eine Gewerkschaft (2)" ist schlichtweg falsch; der Code, von dem er behauptet, er sei legal, ist es nicht.

29 Stimmen

@davmac: Die Autoren von C89 hatten nie die Absicht, die Programmierer zu zwingen, durch Reifen zu springen. Ich finde die Vorstellung, dass eine Regel, die nur zum Zweck der Optimierung existiert, so interpretiert werden sollte, dass Programmierer gezwungen sind, Code zu schreiben, der Daten redundant kopiert, in der Hoffnung, dass ein Optimierer den redundanten Code entfernt, äußerst bizarr.

195voto

Shafik Yaghmour Punkte 147749

Hinweis

Dies ist ein Auszug aus meinem "Was ist die Strict Aliasing Rule und warum ist sie wichtig?" schreiben.

Was ist striktes Aliasing?

In C und C++ hat Aliasing damit zu tun, über welche Ausdruckstypen wir auf gespeicherte Werte zugreifen dürfen. Sowohl in C als auch in C++ legt die Norm fest, welche Ausdruckstypen welche Typen aliasieren dürfen. Der Compiler und der Optimierer dürfen davon ausgehen, dass wir die Aliasing-Regeln strikt befolgen, daher der Begriff strenge Aliasing-Regel . Wenn wir versuchen, auf einen Wert mit einem nicht zulässigen Typ zuzugreifen, wird er als undefiniertes Verhalten ( UB ). Sobald wir ein undefiniertes Verhalten haben, sind die Ergebnisse unseres Programms nicht mehr zuverlässig.

Leider erhalten wir bei strikten Aliasing-Verletzungen oft die Ergebnisse, die wir erwarten, so dass die Möglichkeit besteht, dass eine zukünftige Version eines Compilers mit einer neuen Optimierung den Code, den wir für gültig hielten, kaputt macht. Dies ist nicht wünschenswert, und es ist ein lohnendes Ziel, die strengen Aliasing-Regeln zu verstehen und zu wissen, wie man ihre Verletzung vermeiden kann.

Um zu verstehen, warum uns das wichtig ist, werden wir Probleme erörtern, die auftreten, wenn strenge Aliasing-Regeln verletzt werden, Typ-Punning, da gängige Techniken, die beim Typ-Punning verwendet werden, oft strenge Aliasing-Regeln verletzen, und wie man Typ-Punning korrekt durchführt.

Vorläufige Beispiele

Schauen wir uns einige Beispiele an, dann können wir darüber sprechen, was die Norm(en) genau besagen, einige weitere Beispiele untersuchen und dann sehen, wie man striktes Aliasing vermeidet und Verstöße erkennt, die wir übersehen haben. Hier ist ein Beispiel, das nicht überraschen sollte ( Live-Beispiel ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Wir haben eine int* die auf den von einer anderen Person belegten Speicher verweist int und dies ist ein gültiges Aliasing. Der Optimierer muss davon ausgehen, dass Zuweisungen durch ip könnte den Wert aktualisieren, der von x .

Das nächste Beispiel zeigt Aliasing, das zu undefiniertem Verhalten führt ( Live-Beispiel ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

In der Funktion foo wir nehmen eine int* und eine Schwimmer* , in diesem Beispiel nennen wir foo und setzen Sie beide Parameter so, dass sie auf denselben Speicherplatz zeigen, der in diesem Beispiel eine int . Hinweis: Die uminterpretieren_gießen weist den Compiler an, den Ausdruck so zu behandeln, als hätte er den durch seinen Template-Parameter festgelegten Typ. In diesem Fall weisen wir ihn an, den Ausdruck &x als hätte sie die Schrift Schwimmer* . Wir können naiverweise erwarten, dass das Ergebnis der zweiten cout zu sein 0 aber mit aktivierter Optimierung durch -O2 liefern sowohl gcc als auch clang das folgende Ergebnis:

0
1

Das ist zwar nicht zu erwarten, aber durchaus zulässig, da wir ein undefiniertes Verhalten aufgerufen haben. A Schwimmer kann kein gültiges Alias für eine int Objekt. Daher kann der Optimierer davon ausgehen, dass das Konstante 1 bei der Dereferenzierung gespeichert i wird der Rückgabewert sein, da eine Speicherung durch f nicht wirksam auf ein int Objekt. Das Einfügen des Codes in den Compiler-Explorer zeigt, dass genau das passiert( Live-Beispiel ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Der Optimierer verwendet Typbasierte Alias-Analyse (TBAA) setzt voraus. 1 zurückgegeben und verschiebt den konstanten Wert direkt in das Register eax die den Rückgabewert enthält. TBAA verwendet die Regeln der Sprache darüber, welche Typen als Alias erlaubt sind, um das Laden und Speichern zu optimieren. In diesem Fall weiß TBAA, dass ein Schwimmer kann nicht alias und int und optimiert die Belastung durch i .

Nun zum Regelbuch

Was genau besagt die Norm, was wir tun dürfen und was nicht? Die Sprache der Norm ist nicht einfach, daher werde ich versuchen, für jeden Punkt Codebeispiele zu geben, die die Bedeutung verdeutlichen.

Was besagt die C11-Norm?

があります。 C11 Norm heißt es im Abschnitt 6.5 Ausdrücke Absatz 7 :

Auf den gespeicherten Wert eines Objekts darf nur durch einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen hat: 88) - einen Typ, der mit dem effektiven Typ des Objekts kompatibel ist,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- eine qualifizierte Version eines Typs, der mit dem tatsächlichen Typ des Objekts kompatibel ist,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- einen Typ, der dem vorzeichenbehafteten oder vorzeichenlosen Typ entspricht, der dem effektiven Typ des Objekts entspricht,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc/clang hat eine Erweiterung y auch die die Zuordnung von unsigned int* a int* auch wenn sie nicht kompatibel sind.

- einen Typ, der der Typ mit oder ohne Vorzeichen ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- ein Aggregat oder eine Vereinigung, das/die einen der oben genannten Typen zu seinen/ihren Mitgliedern zählt (einschließlich, rekursiv, ein Mitglied eines Unteraggregats oder einer enthaltenen Vereinigung), oder

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- einen Zeichentyp.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Was der C++17-Standardentwurf sagt

Der C++17-Standardentwurf in Abschnitt [basic.lval] Absatz 11 sagt:

Wenn ein Programm versucht, auf den gespeicherten Wert eines Objekts über einen glvalue von einem anderen als einem der folgenden Typen zuzugreifen, ist das Verhalten undefiniert: 63 (11.1) - der dynamische Typ des Objekts,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - eine cv-qualifizierte Version des dynamischen Typs des Objekts,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - ein Typ, der dem dynamischen Typ des Objekts ähnlich ist (wie in 7.5 definiert),

(11.4) - ein Typ mit oder ohne Vorzeichen, der dem dynamischen Typ des Objekts entspricht,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer cv-qualifizierten Version des dynamischen Typs des Objekts entspricht,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - ein Aggregat oder eine Vereinigung, das/die unter seinen/ihren Elementen oder nichtstatischen Datenelementen einen der oben genannten Typen enthält (einschließlich, rekursiv, eines Elements oder nichtstatischen Datenelements eines Unteraggregats oder einer enthaltenen Vereinigung),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - ein Typ, der ein (möglicherweise cv-qualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - ein char, unsigned char oder std::byte Typ.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Erwähnenswert signiertes Zeichen nicht in der obigen Liste enthalten ist, ist dies ein bemerkenswerter Unterschied zu C die besagt einen Zeichentyp .

Was ist Type Punning?

Wir sind an diesem Punkt angelangt und fragen uns vielleicht, warum wir einen Alias verwenden wollen? Die Antwort lautet normalerweise Wortspiel Oft verletzen die verwendeten Methoden strenge Aliasing-Regeln.

Manchmal möchte man das Typsystem umgehen und ein Objekt als einen anderen Typ interpretieren. Dies wird als Typenschreibweise , um ein Speichersegment in einen anderen Typ umzudeuten. Typenschreibweise ist nützlich für Aufgaben, die Zugriff auf die zugrundeliegende Darstellung eines Objekts zur Ansicht, zum Transport oder zur Manipulation benötigen. Typische Bereiche, in denen Type Punning eingesetzt wird, sind Compiler, Serialisierung, Netzwerkcode usw

Traditionell wird dies erreicht, indem man die Adresse des Objekts nimmt, sie in einen Zeiger des Typs umwandelt, als den wir sie neu interpretieren wollen, und dann auf den Wert zugreift, oder mit anderen Worten durch Aliasing. Zum Beispiel:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Wie wir bereits gesehen haben, ist dies kein gültiges Aliasing, so dass wir ein undefiniertes Verhalten aufrufen. Aber traditionell haben die Compiler keine strengen Aliasing-Regeln verwendet, und diese Art von Code funktionierte in der Regel einfach, und die Entwickler haben sich leider daran gewöhnt, die Dinge auf diese Weise zu tun. Eine gängige alternative Methode für Typ-Punning ist die Verwendung von Unions, die in C gültig ist, aber undefiniertes Verhalten in C++ ( siehe Live-Beispiel ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Dies ist in C++ nicht gültig, und einige sind der Ansicht, dass der Zweck von Unions ausschließlich in der Implementierung von Variantentypen besteht, und halten die Verwendung von Unions für Typ-Punning für einen Missbrauch.

Wie schreibt man Pun richtig?

Die Standardmethode für Typenschreibweise ist sowohl in C als auch in C++ memcpy . Dies mag ein wenig schwerfällig erscheinen, aber der Optimierer sollte die Verwendung von memcpy para Typenschreibweise und optimieren Sie es weg und erzeugen Sie einen Umzug von Register zu Register. Wenn wir zum Beispiel wissen int64_t die gleiche Größe hat wie doppelt :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

können wir memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Bei einem ausreichenden Optimierungsgrad erzeugt jeder anständige moderne Compiler identischen Code wie der zuvor erwähnte uminterpretieren_gießen Methode oder Gewerkschaft Methode für Typenschreibweise . Wenn wir den generierten Code untersuchen, sehen wir, dass er nur das Register mov verwendet ( live Compiler Explorer Beispiel ).

C++20 und bit_cast

In C++20 können wir gewinnen bit_cast ( Umsetzung im Link des Vorschlags verfügbar ), die einen einfachen und sicheren Weg zu type-pun bietet und auch in einem constexpr-Kontext verwendet werden kann.

Im Folgenden wird ein Beispiel für die Verwendung von bit_cast ein Wortspiel zu tippen unsigned int a Schwimmer , ( es live sehen ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

In dem Fall, dass An y Von Typen nicht die gleiche Größe haben, müssen wir eine Zwischenstruktur15 verwenden. Wir werden eine Struktur verwenden, die eine sizeof( unsigned int ) Zeichenanordnung ( geht von 4 Byte unsigned int aus ) als die Von Typ und unsigned int als die An tippen:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Es ist bedauerlich, dass wir diesen Zwischentyp brauchen, aber das ist die derzeitige Einschränkung von bit_cast .

Abfangen von Strict Aliasing-Verletzungen

Wir haben nicht viele gute Werkzeuge, um striktes Aliasing in C++ zu erkennen. Die Werkzeuge, die wir haben, erkennen einige Fälle von strikten Aliasing-Verletzungen und einige Fälle von falsch ausgerichteten Lade- und Speichervorgängen.

gcc unter Verwendung des Flags -fstrict-aliasing y -Strich-Aliasing kann einige Fälle erfassen, wenn auch nicht ohne falsch positive/negative Ergebnisse. Zum Beispiel werden die folgenden Fälle eine Warnung in gcc erzeugen ( es live sehen ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

obwohl es diesen zusätzlichen Fall nicht erfassen wird ( es live sehen ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Obwohl clang diese Flags zulässt, implementiert es die Warnungen offenbar nicht wirklich.

Ein weiteres Werkzeug, das uns zur Verfügung steht, ist ASan, das falsch ausgerichtete Lasten und Lager aufspüren kann. Obwohl es sich hierbei nicht direkt um strikte Aliasing-Verletzungen handelt, sind sie eine häufige Folge von strikten Aliasing-Verletzungen. Die folgenden Fälle erzeugen zum Beispiel Laufzeitfehler, wenn sie mit Clang unter Verwendung von -fsanitize=Adresse

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Das letzte Werkzeug, das ich empfehlen werde, ist C++-spezifisch und kein Werkzeug im engeren Sinne, sondern eine Programmierpraxis: Lassen Sie keine Casts im Stil von C zu. Sowohl gcc als auch clang erzeugen eine Diagnose für Casts im C-Stil mit -Wildnis-Guss . Dadurch werden alle undefinierten Typ-Puns gezwungen, reinterpret_cast zu verwenden. Im Allgemeinen sollte reinterpret_cast ein Kennzeichen für eine genauere Codeüberprüfung sein. Es ist auch einfacher, Ihre Codebasis nach reinterpret_cast zu durchsuchen, um eine Prüfung durchzuführen.

Für C haben wir alle bereits abgedeckten Werkzeuge und außerdem tis-interpreter, einen statischen Analyzer, der ein Programm für eine große Teilmenge der Sprache C vollständig analysiert. Angenommen, eine C-Version des früheren Beispiels, bei der mit -fstrict-aliasing übersieht einen Fall ( es live sehen )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter ist in der Lage, alle drei zu fangen, das folgende Beispiel ruft tis-kernal als tis-interpreter auf (die Ausgabe ist der Kürze halber editiert):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Schließlich gibt es TySan das sich derzeit in der Entwicklung befindet. Dieser Sanitizer fügt Informationen zur Typüberprüfung in ein Schattenspeichersegment ein und prüft Zugriffe darauf, ob sie gegen Aliasing-Regeln verstoßen. Das Tool sollte potenziell in der Lage sein, alle Aliasing-Verletzungen zu erkennen, kann aber einen großen Laufzeit-Overhead haben.

0 Stimmen

Kommentare sind nicht für eine ausführliche Diskussion gedacht; dieses Gespräch wurde in den Chat verschoben .

9 Stimmen

Wenn ich könnte, +10, gut geschrieben und erklärt, auch von beiden Seiten, Compiler-Autoren und Programmierer... die einzige Kritik: Es wäre schön, wenn man oben Gegenbeispiele hätte, um zu sehen, was der Standard verbietet, es ist irgendwie nicht offensichtlich :-)

4 Stimmen

Sehr gute Antwort. Ich bedaure nur, dass die anfänglichen Beispiele in C++ gegeben werden, was es für Leute wie mich, die nur C kennen oder sich dafür interessieren und keine Ahnung haben, was sie tun sollen, schwierig macht, zu folgen. reinterpret_cast tun könnte oder was cout bedeuten könnte. (Es ist in Ordnung, C++ zu erwähnen, aber die ursprüngliche Frage bezog sich auf C, und diese Beispiele könnten genauso gut in C geschrieben werden).

153voto

Ben Voigt Punkte 268424

Dies ist die strenge Aliasing-Regel, die in Abschnitt 3.10 der C++03 Norm (andere Antworten geben gute Erklärungen, aber keine enthält die Regel selbst):

Wenn ein Programm versucht, auf den gespeicherten Wert eines Objekts über einen l-Wert zuzugreifen, der nicht einem der folgenden Typen entspricht, ist das Verhalten undefiniert:

  • der dynamische Typ des Objekts,
  • eine cv-qualifizierte Version des dynamischen Typs des Objekts,
  • einen Typ, der dem vorzeichenbehafteten oder vorzeichenlosen Typ entspricht, der dem dynamischen Typ des Objekts entspricht,
  • einen Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer cv-qualifizierten Version des dynamischen Typs des Objekts entspricht,
  • ein Aggregat- oder Vereinigungstyp, der einen der vorgenannten Typen zu seinen Mitgliedern zählt (einschließlich, rekursiv, eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung),
  • ein Typ, der ein (möglicherweise cv-qualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
  • a char o unsigned char Typ.

C++11 y C++14 Wortlaut (Änderungen hervorgehoben):

Wenn ein Programm versucht, auf den gespeicherten Wert eines Objekts über eine glvalue von einem anderen als einem der folgenden Typen ist das Verhalten undefiniert:

  • der dynamische Typ des Objekts,
  • eine cv-qualifizierte Version des dynamischen Typs des Objekts,
  • einen Typ, der dem dynamischen Typ des Objekts ähnlich ist (wie in 4.4 definiert),
  • einen Typ, der dem vorzeichenbehafteten oder vorzeichenlosen Typ entspricht, der dem dynamischen Typ des Objekts entspricht,
  • einen Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer cv-qualifizierten Version des dynamischen Typs des Objekts entspricht,
  • einen Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Typen enthält Elemente oder nicht-statische Datenelemente (einschließlich, rekursiv, einer Element oder nicht-statisches Datenelement eines Teilaggregats oder einer enthaltenen Vereinigung),
  • ein Typ, der ein (möglicherweise cv-qualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
  • a char o unsigned char Typ.

Zwei Änderungen waren geringfügig: glvalue 代わりに l-Wert und die Klärung des Falles Aggregat/Gewerkschaft.

Die dritte Änderung bietet eine stärkere Garantie (Lockerung der strengen Aliasing-Regel): Das neue Konzept der ähnliche Arten die nun sicher als Alias verwendet werden können.


Auch die C Wortlaut (C99; ISO/IEC 9899:1999 6.5/7; der exakt gleiche Wortlaut wird in ISO/IEC 9899:2011 §6.5 ¶7 verwendet):

Der Zugriff auf den gespeicherten Wert eines Objekts darf nur über einen l-Wert erfolgen Ausdruck zugegriffen werden, der einen der folgenden Typen hat 73) oder 88) :

  • einen Typ, der mit dem effektiven Typ des Objekts kompatibel ist,
  • eine qualifizierte Version eines Typs, der mit dem effektiven Typ von des Objekts,
  • einen Typ, der der Typ mit oder ohne Vorzeichen ist, der dem effektiven Typ des Objekts entspricht,
  • einen Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht,
  • einen Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen enthält Typen zu seinen Mitgliedern zählt (einschließlich, rekursiv, ein Mitglied eines Unteraggregats oder einer enthaltenen Vereinigung), oder
  • einen Zeichentyp.

73) oder 88) Der Zweck dieser Liste ist es, die Umstände zu spezifizieren, unter denen ein Objekt aliasiert werden kann oder nicht.

0 Stimmen

@Kos: Das ist cool, danke! Kannst du dich auch dazu äußern, ob striktes Aliasing von C89/90 gefordert wurde? (Ich glaube mich nicht zu erinnern, dass es zur gleichen Zeit eingeführt wurde wie die restrict Schlüsselwort, aber ich bin mir nicht sicher).

1 Stimmen

Sehen Sie sich die C89-Begründung an cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf Abschnitt 3.3, in dem davon die Rede ist.

0 Stimmen

Wie können Sie einen l-Wert der Klasse type verwenden?

50voto

phorgan1 Punkte 1574

Striktes Aliasing bezieht sich nicht nur auf Zeiger, sondern auch auf Referenzen. Ich habe einen Artikel darüber für das Boost-Entwickler-Wiki geschrieben, der so gut ankam, dass ich ihn in eine Seite auf meiner Beratungs-Website verwandelt habe. Dort wird ausführlich erklärt, was es ist, warum es die Leute so verwirrt und was man dagegen tun kann. Strict Aliasing Weißbuch . Insbesondere wird erklärt, warum Unions für C++ ein riskantes Verhalten darstellen und warum die Verwendung von memcpy die einzige Lösung ist, die sowohl für C als auch für C++ geeignet ist. Ich hoffe, das ist hilfreich.

3 Stimmen

" Striktes Aliasing bezieht sich nicht nur auf Zeiger, sondern auch auf Referenzen " Eigentlich bezieht sie sich auf lWerte . " die Verwendung von memcpy ist die einzige tragbare Lösung " Hört!

4 Stimmen

Der Abschnitt "Another Broken version, referencing a twice" des Papiers macht keinen Sinn. Selbst wenn es einen Sequenzpunkt gäbe, würde er nicht das richtige Ergebnis liefern. Vielleicht wollten Sie Shift-Operatoren anstelle von Shift-Zuweisungen verwenden? Aber dann ist der Code wohldefiniert und macht das Richtige.

6 Stimmen

Gutes Papier. Meine Meinung: (1) dieses Aliasing-'Problem' ist eine Überreaktion auf schlechte Programmierung - der Versuch, den schlechten Programmierer vor seinen schlechten Gewohnheiten zu schützen. Wenn der Programmierer gute Gewohnheiten hat, dann ist dieses Aliasing nur ein Ärgernis und die Überprüfungen können sicher ausgeschaltet werden. (2) Compilerseitige Optimierung sollte nur in wohlbekannten Fällen durchgeführt werden und sollte sich im Zweifelsfall strikt an den Quellcode halten; den Programmierer zu zwingen, Code zu schreiben, um die Eigenheiten des Compilers zu berücksichtigen, ist, einfach ausgedrückt, falsch. Noch schlimmer ist es, sie zum Bestandteil des Standards zu machen.

37voto

Ingo Blackman Punkte 971

Als Ergänzung zu dem, was Doug T. bereits geschrieben hat, hier ist ein einfacher Testfall, der wahrscheinlich mit gcc ausgelöst wird:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Kompilieren mit gcc -O2 -o check check.c . Normalerweise (mit den meisten gcc-Versionen, die ich ausprobiert habe) gibt dies ein "strict aliasing problem" aus, weil der Compiler annimmt, dass "h" nicht die gleiche Adresse wie "k" in der "check"-Funktion sein kann. Aus diesem Grund optimiert der Compiler die if (*h == 5) weg und ruft immer die printf.

Für diejenigen, die daran interessiert sind, hier ist die x64 Assembler-Code, produziert von gcc 4.6.3, läuft auf ubuntu 12.04.2 für x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Die if-Bedingung ist also vollständig aus dem Assembler-Code verschwunden.

0 Stimmen

Wenn Sie ein zweites kurzes * j zu check() hinzufügen und es verwenden ( *j = 7 ) dann Optimierung disapear seit ggc nicht, wenn h und j nicht actualy Punkt zum gleichen Wert sind. ja Optimierung ist wirklich intelligent.

2 Stimmen

Um die Sache lustiger zu machen, können Sie Zeiger auf Typen verwenden, die nicht kompatibel sind, aber die gleiche Größe und Darstellung haben (auf einigen Systemen gilt das z.B. für long long* y int64_t *). Man könnte erwarten, dass ein vernünftiger Compiler erkennen sollte, dass ein long long* y int64_t* könnten auf denselben Speicher zugreifen, wenn sie identisch gespeichert sind, aber eine solche Behandlung ist nicht mehr zeitgemäß.

2 Stimmen

Grr... x64 ist eine Microsoft-Konvention. Verwenden Sie stattdessen amd64 oder x86_64.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X