Wywołanie funkcji ze wskaźnikiem na non-const i wskaźnikiem na const argumentów o tym samym adresie

14

Chcę napisać funkcję, która wprowadza tablicę danych i wyprowadza inną tablicę danych za pomocą wskaźników.

Zastanawiam się, co jest wynikiem zarówno jeśli srci dstwskazał na samym adresem, bo wiem, kompilator może zoptymalizować dla const. Czy jest to niezdefiniowane zachowanie? (Oznacziłem zarówno C, jak i C ++, ponieważ nie jestem pewien, czy odpowiedź może się między nimi różnić, i chcę o nich wiedzieć.)

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

Czy oprócz powyższego pytania jest to dobrze zdefiniowane, jeśli usunę constoryginalny kod?

Willy
źródło

Odpowiedzi:

17

Chociaż prawdą jest, że zachowanie jest dobrze zdefiniowane - to nie prawda, że kompilatory mogą „Optymalizacja dla const” w tym sensie, że masz na myśli.

Oznacza to, że kompilator nie może zakładać, że tylko dlatego, że parametrem jest a const T* ptr, wskazana przez pamięć ptrnie zostanie zmieniona za pomocą innego wskaźnika. Wskaźniki nawet nie muszą być równe. constJest obowiązkiem, a nie gwarancja - obowiązek przez Ciebie (= funkcja) nie dokonywać zmian przez ten wskaźnik.

Aby faktycznie mieć tę gwarancję, musisz oznaczyć wskaźnik restrictsłowem kluczowym. Zatem jeśli skompilujesz te dwie funkcje:

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

foo()funkcja musi czytać dwukrotnie z x, podczas gdy bar()tylko musi ją przeczytać raz:

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

Zobacz to na żywo GodBolt.

restrictjest tylko słowem kluczowym w C (od C99); niestety do tej pory nie został wprowadzony do C ++ (z tego słabszego powodu, że bardziej skomplikowane jest wprowadzenie go w C ++). Jednak wiele kompilatorów trochę to obsługuje __restrict.

Konkluzja: Kompilator musi obsługiwać Twój „ezoteryczny” przypadek użycia podczas kompilacji f()i nie będzie miał z tym żadnego problemu.


Zobacz ten post dotyczący przypadków użycia dla restrict.

einpoklum
źródło
constnie jest „obowiązkiem użytkownika (= funkcji), aby nie dokonywać zmian za pomocą tego wskaźnika”. Standard C pozwala na usunięcie funkcji constza pomocą rzutowania, a następnie zmodyfikowanie obiektu w wyniku. Zasadniczo constjest jedynie poradą i wygodą dla programisty, aby pomóc uniknąć przypadkowej modyfikacji obiektu.
Eric Postpischil
@EricPostpischil: To obowiązek, z którego możesz się wywiązać.
einpoklum
Obowiązek, z którego możesz się wydostać, nie jest obowiązkiem.
Eric Postpischil
2
@EricPostpischil: 1. Rozdzielasz włosy tutaj. 2. To nie prawda.
einpoklum
1
Oto dlaczego memcpyi strcpysą zadeklarowane z restrictargumentami, podczas gdy memmovenie jest - tylko ten ostatni pozwala na nakładanie się bloków pamięci.
Barmar
5

Jest to dobrze zdefiniowane (w C ++, już nie jestem pewien w C), z constkwalifikatorem i bez niego .

Pierwszą rzeczą, której należy szukać, jest ścisła zasada aliasingu 1 . Jeśli srci dstwskazuje na ten sam obiekt:

Jeśli chodzi o constkwalifikator, możesz argumentować, że skoro dst == srctwoja funkcja skutecznie modyfikuje to, na co srcwskazuje, srcnie powinna być kwalifikowana jako const. To nie tak constdziała. Należy wziąć pod uwagę dwa przypadki:

  1. Gdy obiekt jest zdefiniowany constjako taki char const data[42];, modyfikacja (bezpośrednio lub pośrednio) prowadzi do niezdefiniowanego zachowania.
  2. Kiedy constzdefiniowane jest odniesienie lub wskaźnik do obiektu, tak jak w char const* pdata = data;, można zmodyfikować obiekt leżący u jego podstaw, pod warunkiem, że nie został on zdefiniowany jako const2 (patrz 1.). Zatem następujące elementy są dobrze zdefiniowane:
int main()
{
    int result = 42;
    int const* presult = &result;
    *const_cast<int*>(presult) = 0;
    return *presult; // 0
}

1) Jaka jest ścisła zasada aliasingu?
2) Czy jest const_castbezpieczny?

YSC
źródło
Może PO oznacza możliwą zmianę kolejności zadań?
Igor R.
char*i char const*nie są kompatybilne. _Generic((char *) 0, const char *: 1, default: 0))ocenia na zero.
Eric Postpischil
Sformułowanie „Po constzdefiniowaniu odwołania lub wskaźnika do obiektu” jest niepoprawne. Masz na myśli to, że gdy zdefiniowane jest odwołanie lub wskaźnik do typuconst kwalifikowanego , nie oznacza to, że obiekt, na który ma wskazywać, nie może być modyfikowany (na różne sposoby). (Jeśli wskaźnik wskazuje na obiekt, oznacza to, że obiekt rzeczywiście jest z definicji, więc zachowanie próby modyfikacji nie jest zdefiniowane.)constconst
Eric Postpischil
@Eric, jestem konkretny tylko wtedy, gdy pytanie dotyczy standardu lub jest oznaczone language-lawyer. Dokładność to wartość, którą cenię, ale zdaję sobie również sprawę, że wiąże się z większą złożonością. Tutaj zdecydowałem się na prostotę i zrozumiałe zdania, ponieważ uważam, że to właśnie chciał OP. Jeśli uważasz inaczej, proszę odpowiedz, będę jednym z pierwszych, który zagłosuje. W każdym razie dziękuję za komentarz.
YSC
3

Jest to dobrze zdefiniowane w C. Ścisłe reguły aliasingu nie mają zastosowania do chartypu ani do dwóch wskaźników tego samego typu.

Nie jestem pewien, co masz na myśli przez „optymalizuj dla const”. Mój kompilator (GCC 8.3.0 x86-64) generuje dokładnie ten sam kod dla obu przypadków. Jeśli dodasz restrictspecyfikator do wskaźników, wygenerowany kod będzie nieco lepszy, ale to nie zadziała w twoim przypadku, ponieważ wskaźniki są takie same.

(C11 § 6.5 7)

Obiekt ma swoją zapisana wartość dostępne tylko lwartością wyrażenia, które ma jeden z następujących typów:
- typ zgodny z efektywnym typem obiektu,
- wykwalifikowana wersja typu zgodnego z efektywnym typem obiektu,
- typ, który jest typem podpisanym lub niepodpisanym odpowiadającym efektywnemu typowi obiektu,
- typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowanej wersji efektywnego typu obiektu,
- typ agregacyjny lub łączny, który obejmuje jeden wyżej wymienionych typów wśród jej członków (w tym rekurencyjnie członek subagregatu lub zawartego związku), lub
- typ postaci.

W takim przypadku (bez restrict) zawsze otrzymasz 121wynik.

SS Anne
źródło