Funktionsaufruf mit Zeiger auf Nicht-Konstanten und Zeiger auf Konstantenargumente derselben Adresse

14

Ich möchte eine Funktion schreiben, die ein Datenarray eingibt und ein anderes Datenarray mithilfe von Zeigern ausgibt.

Ich frage mich, was das Ergebnis ist, wenn beide srcund dstauf dieselbe Adresse zeigen, weil ich weiß, dass der Compiler für const optimieren kann. Ist es undefiniertes Verhalten? (Ich habe sowohl C als auch C ++ markiert, weil ich nicht sicher bin, ob die Antwort zwischen ihnen unterschiedlich sein kann, und ich möchte über beide Bescheid wissen.)

void f(const char *src, char *dst) {
    dst[2] = src[0];
    dst[1] = src[1];
    dst[0] = src[2];
}

int main() {
    char s[] = "123";
    f(s,s);
    printf("%s\n", s);
    return 0;
}

Ist dies zusätzlich zu der obigen Frage genau definiert, wenn ich den constOriginalcode lösche ?

Willy
quelle

Antworten:

17

Es stimmt zwar, dass das Verhalten genau definiert ist, aber es stimmt nicht , dass Compiler in dem von Ihnen gemeinten Sinne "für const optimieren" können.

Das heißt, ein Compiler darf nicht davon ausgehen, dass nur weil ein Parameter a ist const T* ptr, der Speicher, auf den von ptrzeigt, nicht durch einen anderen Zeiger geändert wird. Die Zeiger müssen nicht einmal gleich sein. Dies constist eine Verpflichtung, keine Garantie - eine Verpflichtung von Ihnen (= die Funktion), keine Änderungen über diesen Zeiger vorzunehmen.

Um diese Garantie tatsächlich zu haben, müssen Sie den Zeiger mit dem restrictSchlüsselwort markieren . Wenn Sie also diese beiden Funktionen kompilieren:

int foo(const int* x, int* y) {
    int result = *x;
    (*y)++;
    return result + *x;
}

int bar(const int* x, int* restrict y) {
    int result = *x;
    (*y)++;
    return result + *x;
}

Die foo()Funktion muss zweimal lesen x, während sie bar()nur einmal lesen muss:

foo:
        mov     eax, DWORD PTR [rdi]
        add     DWORD PTR [rsi], 1
        add     eax, DWORD PTR [rdi]  # second read
        ret
bar:
        mov     eax, DWORD PTR [rdi]
        add     DWORD PTR [rsi], 1
        add     eax, eax              # no second read
        ret

Sehen Sie dies live weiter GodBolt.

restrictist nur ein Schlüsselwort in C (seit C99); Leider wurde es bisher nicht in C ++ eingeführt (aus dem schlechten Grund, dass die Einführung in C ++ komplizierter ist). Viele Compiler unterstützen es jedoch irgendwie als __restrict.

Fazit: Der Compiler muss Ihren "esoterischen" Anwendungsfall beim Kompilieren f()unterstützen und hat keine Probleme damit.


Siehe diesen Beitrag zu Anwendungsfällen für restrict.

einpoklum
quelle
constist nicht „eine Verpflichtung von Ihnen (= der Funktion), keine Änderungen über diesen Zeiger vorzunehmen“. Der C-Standard ermöglicht es der Funktion, constüber einen Cast zu entfernen und das Objekt dann durch das Ergebnis zu modifizieren. Dies constist im Wesentlichen nur eine Empfehlung und eine Annehmlichkeit für den Programmierer, um zu vermeiden, dass ein Objekt versehentlich geändert wird.
Eric Postpischil
@EricPostpischil: Es ist eine Verpflichtung, aus der du herauskommen kannst.
Einpoklum
Eine Verpflichtung, aus der Sie herauskommen können, ist keine Verpflichtung.
Eric Postpischil
2
@EricPostpischil: 1. Du spaltest hier Haare. 2. Das stimmt nicht.
Einpoklum
1
Dies ist der Grund memcpyund strcpywird mit restrictArgumenten deklariert , während dies memmovenicht der Fall ist - nur letzteres ermöglicht eine Überlappung zwischen den Speicherblöcken.
Barmar
5

Dies ist genau definiert (in C ++ nicht mehr sicher in C), mit und ohne constQualifizierer.

Das erste, wonach Sie suchen müssen, ist die strenge Aliasing-Regel 1 . If srcund dstzeigt auf dasselbe Objekt:

In Bezug auf das constQualifikationsmerkmal könnten Sie argumentieren, dass dst == srcSie nicht qualifiziert werden sollten , wenn Ihre Funktion effektiv ändert, auf welche srcPunkte verwiesen wird . So funktioniert das nicht. Zwei Fälle müssen berücksichtigt werden:srcconstconst

  1. Wenn ein Objekt constwie in definiert ist , char const data[42];führt das Ändern (direkt oder indirekt) zu undefiniertem Verhalten.
  2. Wenn eine Referenz oder ein Zeiger auf ein constObjekt wie in definiert ist char const* pdata = data;, kann das zugrunde liegende Objekt geändert werden, sofern es nicht als const2 definiert wurde (siehe 1.). Folgendes ist also genau definiert:
int main()
{
    int result = 42;
    int const* presult = &result;
    *const_cast<int*>(presult) = 0;
    return *presult; // 0
}

1) Was ist die strenge Aliasing-Regel?
2) Ist const_castsicher?

YSC
quelle
Vielleicht bedeutet das OP eine mögliche Neuordnung der Aufgaben?
Igor R.
char*und char const*sind nicht kompatibel. _Generic((char *) 0, const char *: 1, default: 0))ergibt null.
Eric Postpischil
Die Formulierung „Wenn eine Referenz oder ein Zeiger auf ein constObjekt definiert ist“ ist falsch. Sie meinen, wenn eine Referenz oder ein Zeiger auf einen constqualifizierten Typ definiert ist, bedeutet dies nicht, dass das Objekt, auf das es zeigen soll, möglicherweise nicht geändert wird (auf verschiedene Weise). (Wenn der Zeiger auf ein constObjekt zeigt, bedeutet dies, dass das Objekt tatsächlich constper Definition ist, sodass das Verhalten beim Versuch, es zu ändern, nicht definiert ist.)
Eric Postpischil
@ Eric, ich bin nur so spezifisch, wenn die Frage über Standard oder getaggt ist language-lawyer. Genauigkeit ist ein Wert, den ich schätze, aber ich bin mir auch bewusst, dass er komplexer ist. Hier habe ich mich für Einfachheit und leicht verständliche Sätze entschieden, weil ich glaube, dass dies das ist, was OP wollte. Wenn Sie etwas anderes denken, antworten Sie bitte, ich bin einer der Ersten, die es positiv bewerten. Trotzdem danke für deinen Kommentar.
YSC
3

Dies ist in C genau definiert. Strikte Aliasing-Regeln gelten weder für den charTyp noch für zwei Zeiger desselben Typs.

Ich bin mir nicht sicher, was Sie unter "Optimieren für const" verstehen . Mein Compiler (GCC 8.3.0 x86-64) generiert in beiden Fällen genau den gleichen Code. Wenn Sie restrictden Zeigern den Bezeichner hinzufügen , ist der generierte Code etwas besser, aber das funktioniert in Ihrem Fall nicht, da die Zeiger gleich sind.

(C11 §6.5 7)

Auf den gespeicherten Wert eines Objekts darf nur über einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen aufweist:
- einen Typ, der mit dem effektiven Typ des Objekts kompatibel ist,
- eine qualifizierte Version eines Typs, der mit dem effektiven Typ des Objekts kompatibel ist,
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem effektiven Typ des Objekts entspricht,
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht,
- ein Aggregat- oder Vereinigungstyp, der einen enthält der oben genannten Typen unter seinen Mitgliedern (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung) oder
- eines Zeichentyps.

In diesem Fall (ohne restrict) erhalten Sie immer 121als Ergebnis.

SS Anne
quelle