2016-02-26 12 views
37

Ich stolperte über ein kleines Problem, während ich versuchte, const-korrekten Code zu machen.Kann GCC mich warnen, die Felder einer Const-Struktur in C99 zu modifizieren?

Ich hätte gerne eine Funktion geschrieben, die einen Zeiger auf eine const-Struktur nimmt, um dem Compiler zu sagen "Bitte sagen Sie mir, wenn ich die Struktur modifiziere, weil ich wirklich nicht will".

Es kam plötzlich zu meinem Kopf, dass die Compiler mir, dies tun können:

struct A 
{ 
    char *ptrChar; 
}; 

void f(const struct A *ptrA) 
{ 
    ptrA->ptrChar[0] = 'A'; // NOT DESIRED!! 
} 

was verständlich ist, da tatsächlich was konst der Zeiger selbst ist, aber nicht der Typ, es verweist. Ich möchte, dass der Compiler mir sagt, dass ich etwas mache, was ich nicht tun möchte, wenn das überhaupt möglich ist.

Ich habe gcc als meinen Compiler verwendet. Obwohl ich weiß, dass der obige Code legal sein sollte, überprüfte ich trotzdem, ob es trotzdem eine Warnung geben würde, aber nichts kam. Meine Befehlszeile war:

gcc -std=c99 -Wall -Wextra -pedantic test.c 

Ist es möglich, um dieses Problem zu umgehen? Kein

+0

dieser Fall funktioniert zur Zuweisung von Mitglied. ZB "ptrA-> ptrChar = malloc (2);' Welches Element zeigt, ist nicht so. " – BLUEPIXY

+0

[Ähnliche Frage] (http://StackOverflow.com/Questions/13181546/const-correctness-for-structs-with-pointers) –

+0

Zu diesem Zweck habe ich normalerweise einen Header, der nur nach vorne deklariert 'struct a;', und einige Funktionsdeklarationen wie in 'a_do_something (const struct * a);' Die vollständige Definition von 'struct A' in eine Header-Datei ist nur selten erforderlich. Sie setzen eine Menge Funktionen wie Ihr 'f' in separate Übersetzungseinheiten, wobei' f' nur irgendwie ptrA verwendet wie 'stuff = a_do_etwas (ptrA);' –

Antwort

19

Eine Möglichkeit, dies zu umgehen, besteht darin, bei Bedarf zwei verschiedene Typen für dasselbe Objekt zu verwenden: einen Lese-/Schreib-Typ und einen schreibgeschützten Typ.

typedef struct 
{ 
    char *ptrChar; 
} A_rw; 

typedef struct 
{ 
    const char* ptrChar; 
} A_ro; 


typedef union 
{ 
    A_rw rw; 
    A_ro ro; 
} A; 

Wenn eine Funktion, das Objekt ändern muss, nimmt sie den Schreib-Lese-Typen als Parameter, sonst nimmt es den Nur-Lese-Typen.

void modify (A_rw* a) 
{ 
    a->ptrChar[0] = 'A'; 
} 

void print (const A_ro* a) 
{ 
    puts(a->ptrChar); 
} 

den Anrufer-Schnittstelle, um ziemlich hoch und es konsistent machen, können Sie Wrapper-Funktionen wie die öffentliche Schnittstelle zu Ihrem ADT verwenden können:

inline void A_modify (A* a) 
{ 
    modify(&a->rw); 
} 

inline void A_print (const A* a) 
{ 
    print(&a->ro); 
} 

Mit dieser Methode kann A nun als undurchsichtige Art umgesetzt werden , um die Implementierung für den Aufrufer zu verbergen.

+1

Ich mag die Gewerkschaft und bin ziemlich sicher, dass es in der Praxis funktionieren wird; ist es erlaubt? Die Strukturtypen ar, iiuc, inkompatibel. –

+0

@ PeterA.Schneider Ich glaube ja, nach der "strikten Aliasing-Regel" (6.5/7) und der Union "type punning rule" (6.2.6.1/7). Letzteres könnte jedoch ein unspezifisches Verhalten sein. Es ist nicht wirklich wichtig, denn wie du sagst, wird dies immer in der Praxis funktionieren. Ich sehe nicht, wie es für den Compiler Sinn machen würde, diesen Code anders als erwartet zu implementieren, ungeachtet irgendwelcher subtiler Regeln im Standard. Es funktioniert jedoch möglicherweise nicht in C++, da iirc C++ das Punning von Typen durch Unionen nicht erlaubt. – Lundin

+5

@Lundin: Vorsicht gcc. Die Optimierung basiert auf Annahmen, die aus der strengen Aliasing-Regel abgeleitet werden können. –

2

, wenn Sie die Struktur Definition zu ändern:

struct A 
{ 
    const char *ptrChar; 
}; 

Eine weitere gewundene Lösung, die die alte Struktur Definition intakt hält, ist eine neue Struktur mit identischen Elementen zu definieren, deren diesbezüglicher Zeiger Mitglieder werden auf: zeigt auf const type. Dann wird die Funktion, die Sie aufrufen, geändert, um die neue Struktur zu übernehmen. Es wird eine Wrapperfunktion definiert, die die alte Struktur übernimmt, ein Element nach Element in die neue Struktur kopiert und an die Funktion übergibt.

+0

Ich frage mich, ob ein einfacher Cast zu einer Struktur nur in cv-Qualifikation der Mitglieder wäre UB? –

+0

@ PeterA.Schneider Ja würde es, die Typen wären inkompatibel. – 2501

7

Dies ist ein Beispiel für die Implementierung im Vergleich zur Schnittstelle oder "Information Hiding" - oder besser gesagt: nicht verstecken ;-) - Problem. In C++ hätte man einfach den Zeiger privat und definiert geeignete öffentliche const-Accessoren. Oder man würde eine abstrakte Klasse - eine "Schnittstelle" - mit dem Accessor definieren. Die eigentliche Struktur würde das umsetzen. Benutzer, die keine Strukturinstanzen erstellen müssen, müssen nur die Schnittstelle sehen.

In C könnte man das emulieren, indem man eine Funktion definiert, die einen Zeiger auf die Struktur als Parameter nimmt und einen Zeiger auf const char zurückgibt. Für Benutzer, die keine Instanzen dieser Strukturen erzeugen, könnte man sogar einen "Benutzerheader" bereitstellen, der die Implementierung der Struktur nicht leckt, sondern nur Manipulationsfunktionen definiert, die Zeiger aufnehmen (oder wie eine Fabrik zurückgeben). Dadurch bleibt die Struktur unvollständig (so dass nur Zeiger auf Instanzen verwendet werden können). Dieses Muster emuliert effektiv, was C++ hinter den Kulissen mit dem Zeiger this macht.

5

Dies ist ein bekanntes Problem der C-Sprache und nicht vermeidbar. Schließlich ändern Sie die Struktur nicht, Sie ändern ein separates Objekt durch einen nicht qualifizierten Zeiger, den Sie von der Struktur erhalten haben. const Semantik wurde ursprünglich um die Notwendigkeit entwickelt, Speicherbereiche als konstant zu markieren, die physikalisch nicht beschreibbar sind, nicht um irgendwelche Bedenken für defensive Programmierung.

5

Wenn Sie sich für die Verwendung von C11 entscheiden, können Sie ein generisches Makro implementieren, das entweder auf die konstante oder variable Version desselben Elements verweist (Sie sollten in Ihrer Struktur auch eine Union einfügen). So etwas wie dieses:

struct A 
{ 
    union { 
     char *m_ptrChar; 

     const char *m_cptrChar; 
    } ; 
}; 

#define ptrChar_m(a) _Generic(a, struct A *: a->m_ptrChar,  \ 
           const struct A *: a->m_cptrChar)//, \ 
           //struct A: a.m_ptrChar,  \ 
           //const struct A: a.m_cptrChar) 

void f(const struct A *ptrA) 
{ 
    ptrChar_m(ptrA) = 'A'; // NOT DESIRED!! 
} 

Die Union erstellt 2 Interpretation für ein einzelnes Mitglied.Die m_cptrChar ist ein Zeiger auf konstante Char und die m_ptrChar zu nicht konstant. Dann entscheidet das Makro, auf welches sich es bezieht, abhängig von der Art seines Parameters.

Das einzige Problem ist, dass das Makro ptrChar_m kann nur entweder mit einem Zeiger oder Objekt dieser Struktur und nicht die beiden arbeiten.

+1

Schöne Lösung, obwohl Sie die '.' Fälle fallen lassen müssen, oder Sie werden Probleme mit der Makroerweiterung bekommen.Vermeiden Sie auch Variablennamen, die mit Unterstrichen beginnen (siehe 7.1.3). – Lundin

+4

Bitte tun Sie das nicht. Das ist schrecklich. Niemand wird das in einem Jahr verstehen, nicht einmal du. – fuz

+0

Ich mag die Union Punning nicht. Könnten wir statt dessen '_Generic (a, Struktur A *: a-> ptrChar, const Struktur A *: (const char *) a-> ptrChar) verwenden? Auf diese Weise müssen wir die Definition der Struktur nicht ändern. – chi

3

Wir könnten die Informationen hinter einigen "Accessor" Funktionen verbergen:

// header 
struct A;   // incomplete type 
char *getPtr(struct A *); 
const char *getCPtr(const struct A *); 

// implementation 
struct A { char *ptr }; 
char *getPtr(struct A *a) { return a->ptr; } 
const char *getCPtr(const struct A *a) { return a->ptr; } 
1

GCC Kann mir warnen über die Felder eines const struct in C99 zu modifizieren?

Sie ändern die Felder einer Const-Struktur nicht.

Ein Wert von Struktur A enthält einen Zeiger auf ein nicht-const Char. ptrA ist ein Zeiger auf eine const-Struktur A. Sie können also den Wert von struct A bei * ptrA nicht ändern. Sie können also den Zeiger nicht auf char (* ptrA) .Char aka ptrA-> ptrChar ändern. Aber Sie ändern den Wert von where ptrA-> ptrChar, also den Wert von * (ptrA-> Char) aka ptrA-> Char [0]. Die einzigen Nachteile hier sind struct As und du änderst keine Struktur A, also was genau ist "nicht erwünscht"?

Wenn Sie nicht möchten, dass bei Änderung des Wertes zu ermöglichen, wo ein Char Feldpunkte der Struktur A (über dieser Struktur A) dann verwenden

struct A 
{ 
    const char *ptrChar; // or char const *ptrChar; 
}; 

Aber vielleicht was Sie denken Sie tun in f ist so etwas wie

void f(const struct A *ptrA) 
{ 
    const char c = 'A'; 
    ptrA->ptrChar = &c; 
} 

Welche wird einen Fehler vom Compiler bekommen.