2013-03-05 15 views
24

Czy następujący kod (który wykonuje arytmetyczną wskazówkę na granicach podobiektów) ma dobrze zdefiniowane zachowanie dla typów T, dla których kompiluje (który w C++ 11, does not not necessarily have to be POD) lub jego podzestawie?Arytmetyka wskaźnikowa na granicach podobiektów

#include <cassert> 
#include <cstddef> 

template<typename T> 
struct Base 
{ 
    // ensure alignment 
    union 
    { 
     T initial; 
     char begin; 
    }; 
}; 

template<typename T, size_t N> 
struct Derived : public Base<T> 
{ 
    T rest[N - 1]; 
    char end; 
}; 

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.initial == 10); 
    assert(&d.end - &d.begin == sizeof(float) * 10); 
    return 0; 
} 

LLVM wykorzystuje wariancie powyższej techniki w celu wykonania wewnętrznego rodzaju wektora, który jest zoptymalizowany do wstępnego użyciu stosu małych tablic, ale przełącza się bufor sterty przydzielana na początkowej mocy. (Powodem robi to w ten sposób nie wynika z tego przykładu, ale to widocznie zmniejszyć kod szablonu uwędzić, to jest wyraźniejsze, jeśli spojrzeć przez code).

UWAGA: Przed ktoś narzeka, to nie jest dokładnie co robią i być może ich podejście jest bardziej zgodne z normami niż to, co tu podałem, ale chciałem zapytać o ogólny przypadek.

Oczywiście działa to w praktyce, ale jestem ciekawy, czy cokolwiek w standardowej gwarancji, aby tak było. Jestem skłonny powiedzieć nie, biorąc pod uwagę N3242/expr.add:

Gdy dwa wskaźniki do elementów tego samego obiektu tablicy są odejmowane, wynik jest różnicą indeksów dwóch elementów tablicy. ..Ponadto, jeśli wyrażenie P wskazuje na element obiektu tablicy lub jeden za ostatnim elementem obiektu tablicy, a wyrażenie Q wskazuje na ostatni element tego samego obiektu tablicy, wyrażenie ((Q) +1) - (P) ma taką samą wartość jak ((Q) - (P)) + 1 i as - ((P) - ((Q) +1)) i ma wartość zero, jeśli wyrażenie P punktów jeden za ostatnim elementem obiektu tablicy, mimo że wyrażenie (Q) +1 nie wskazuje na element obiektu tablicy. ... Jeśli oba wskaźniki nie wskazują elementów tego samego obiektu tablicy lub jednego ostatniego elementu obiektu tablicy, zachowanie jest niezdefiniowane.

Ale teoretycznie, środkowa część z powyższym cytatem, w połączeniu z układem klasy i wyrównania gwarancji, może pozwolić następujące (minor) regulacja ważność:

#include <cassert> 
#include <cstddef> 

template<typename T> 
struct Base 
{ 
    T initial[1]; 
}; 

template<typename T, size_t N> 
struct Derived : public Base<T> 
{ 
    T rest[N - 1]; 
}; 

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.rest[0] == 9); 
    assert(&d.rest[0] == &d.initial[1]); 
    assert(&d.rest[0] - &d.initial[0] == 1); 
    return 0; 
} 

co w połączeniu z różnymi innymi przepisami w odniesieniu do układu union, wymienialność na i od char * itp., może prawdopodobnie sprawić, że oryginalny kod również będzie ważny. (Głównym problemem jest brak przechodniości w definicji arytmetyki wskaźnika podanej powyżej.)

Ktoś wie na pewno? N3242/expr.add wydaje się jasne, że wskaźniki muszą należeć do tego samego "obiektu tablicowego", aby można go było zdefiniować, ale może to być np. Fakt, że inne gwarancje w standardzie, gdy są ze sobą połączone, mogą wymagać definicję w każdym przypadku w tym przypadku, aby zachować logiczną samowystarczalność. (Nie mam zagrać na nim, ale byłoby to co najmniej do pomyślenia.)

EDIT: @MatthieuM podnosi zarzut, że ta klasa nie jest standardowym układ i dlatego nie może być zagwarantowane nie zawierają dopełnienie między podstawowy podobiekt i pierwszy element wyprowadzonego, nawet jeśli oba są wyrównane do alignof(T).Nie jestem pewien, jak to jest prawdziwe, ale otwiera się na następujące pytania Wariant:

  • byłoby to gwarantowane do pracy, czy dziedziczenie zostały usunięte?

  • Czy można uzyskać &d.end - &d.begin >= sizeof(float) * 10, nawet jeśli &d.end - &d.begin == sizeof(float) * 10 nie było?

OSTATNIA EDYCJA @ArneMertz przemawia za bardzo uważnej lektury N3242/expr.add (tak, wiem, czytam projekt, ale jest to na tyle blisko), ale robi naprawdę standard sugeruje, że następujące zachowanie ma niezdefiniowane zachowanie, jeśli linia wymiany zostanie usunięta? (definicje samej klasy jak wyżej)

int main() 
{ 
    Derived<float, 10> d; 
    bool aligned; 
    float * p = &d.initial[0], * q = &d.rest[0]; 

    ++p; 
    if((aligned = (p == q))) 
    { 
     std::swap(p, q); // does it matter if this line is removed? 
     *++p = 1.0; 
    } 

    assert(!aligned || d.rest[1] == 1.0); 

    return 0; 
} 

Ponadto, jeśli == nie jest wystarczająco silny, co będzie, jeśli skorzystamy z faktu, że std::less tworzy sumie uporządkowane nad wskaźnikami i zmienić tryb warunkowy wyżej:

if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p)))) 

Czy kod zakłada, że ​​dwa równe wskaźniki wskazują na ten sam obiekt tablicy, który jest naprawdę uszkodzony zgodnie ze ścisłym odczytem standardu?

EDIT Niestety, po prostu chcą dodać jeszcze jeden przykład, aby wyeliminować problem standardowy układ:

#include <cassert> 
#include <cstddef> 
#include <utility> 
#include <functional> 

// standard layout 
struct Base 
{ 
    float initial[1]; 
    float rest[9]; 
}; 

int main() 
{ 
    Base b; 
    bool aligned; 
    float * p = &b.initial[0], * q = &b.rest[0]; 

    ++p; 
    if((aligned = (p == q))) 
    { 
     std::swap(p, q); // does it matter if this line is removed? 
     *++p = 1.0; 
     q = &b.rest[1]; 
     // std::swap(p, q); // does it matter if this line is added? 
     p -= 2; // is this UB? 
    } 
    assert(!aligned || b.rest[1] == 1.0); 
    assert(p == &b.initial[0]); 

    return 0; 
} 
+6

Nie mogę uwierzyć, że w tagu C++ są dobre pytania. +1. –

+0

Może duplikat [Unia wyrównanie elementu] (http://stackoverflow.com/questions/891471/union-element-alignment), ale nie jestem pewien –

+0

@ BЈовић to pytanie zakłada zrozumienie odpowiedzi na to pytanie, a właściwie –

Odpowiedz

8

Aktualizacja: Ta odpowiedź początkowo brakowało pewnych informacji, a tym samym prowadzić do błędnych wniosków.

W twoich przykładach initial i rest są wyraźnie odrębne (tablica) obiekty, więc porównując wskaźniki do initial (lub jego elementy) ze wskaźnikami do rest (lub jego elementy) jest

  • UB, jeśli używasz różnica wskaźników. (§5.7,6)
  • nieokreślone, jeśli używać operatorów relacyjnych (§5.9,2)
  • dobrze zdefiniowany dla == (So drugi snipped jest dobry, patrz niżej)

Pierwszy fragment:

Budowanie różnicę w pierwszym fragmencie jest niezdefiniowane zachowanie, za cytat podany (§5.7,6):

chyba że obie pointers POIN t do elementów tego samego obiektu tablicowego lub po ostatnim elemencie obiektu tablicy, zachowanie jest niezdefiniowane.

celu wyjaśnienia części UB pierwszego przykładu kodu:

//first example 
int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.initial == 10);   //!!! UB !!! 
    assert(&d.end - &d.begin == sizeof(float) * 10); //!!! UB !!! (*) 
    return 0; 
} 

Linia oznaczona (*) ciekawe: d.begin i d.end nie są elementy o tej samej matrycy, a zatem, że dzięki działaniu UB.Dzieje się tak pomimo faktu, że możesz reinterpret_cast<char*>(&d) i oba mają swoje adresy w wynikowej tablicy. Ale ponieważ tablica ta jest reprezentacją wszystkie z d, nie należy postrzegać jako dostęp do części z d. Tak więc, chociaż operacja prawdopodobnie po prostu zadziała i da oczekiwany rezultat każdej implementacji, o jakiej można marzyć, to nadal jest UB - z definicji.

Drugi fragment:

To jest rzeczywiście dobrze określone zachowanie, ale realizacja zdefiniowane wynik:

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.rest[0] == 9); 
    assert(&d.rest[0] == &d.initial[1]);   //(!) 
    assert(&d.initial[1] - &d.initial[0] == 1); 
    return 0; 
} 

Linia oznaczona (!) jest nie UB, ale jego wynik jest wdrożenie zdefiniowane, ponieważ dopełnienie, wyrównanie i wspomniana instumentacja mogą odgrywać pewną rolę. Ale if, że to twierdzenie będzie trzymać, można użyć dwóch części obiektu, takich jak jedna tablica.

Użytkownik powinien wiedzieć, że rest[0] będzie znajdować się bezpośrednio po initial[0] w pamięci. Na pierwszy rzut oka, nie można łatwo korzystać z równości:

  • initial[1] przypomina jedną past-the-end initial, dereferencji to UB.
  • rest[-1] jest wyraźnie poza zakresem.

Ale wchodzi §3.9.2,3:

Jeśli obiekt typu T znajduje się pod adresem A, wskaźnik typu cvT* którego wartość jest adres Mówi się, że obiekt A wskazuje ten obiekt, niezależnie od tego, jak została uzyskana. [Uwaga: Na przykład: adres jeden za końcem tablicy (5.7) zostanie uznany za wskazujący na niepowiązany obiekt typu elementu tablicy , który może znajdować się pod tym adresem.

Pod warunkiem, że &initial[1] == &rest[0], będzie binarny tak samo, jak gdyby była tylko jedna tablica, a wszystko będzie w porządku.

Można wykonywać iteracje na obu tablicach, ponieważ można zastosować pewien "przełącznik kontekstu wskaźnika" na granicach. Więc do twojego ostatniego fragmentu: swap nie jest potrzebny!

Istnieją jednak pewne zastrzeżenia: rest[-1] to UB, a więc byłoby initial[2], z powodu §5.7,5:

Jeśli zarówno operand wskaźnik i punkt rezultat do elementów tego samego obiekt tablicy lub ostatni element obiektu tablicy, ocena nie może powodować przekroczenia; w przeciwnym razie zachowanie to niezdefiniowana.

(nacisk kopalni). Jak więc te dwa pasują do siebie?

  • „Dobra Droga”: &initial[1] jest ok, a ponieważ &initial[1] == &rest[0] można wziąć ten adres i przejść, aby zwiększyć wskaźnik dostępu do innych elementów rest powodu §3.9.2,3
  • " Zła ścieżka ": initial[2] jest *(initial + 2), ale ponieważ §5.7,5, initial +2 jest już UB i nigdy nie można użyć tutaj §3.9.2.3.

Razem: musisz zatrzymać się na granicy, zrobić krótką przerwę, aby sprawdzić, czy adresy są równe, a następnie możesz iść dalej.

+0

wierzę ci, ale to oznaczałoby, że to niemożliwe wdrożenie ' std :: memset' lub coś podobnego do siebie bez wywoływania UB, niezależnie od tego, czy obiekt był układem standardowym czy nie? Musiałbyś użyć dostarczonych funkcji jako prymitywów, czy też? –

+0

(Nawiasem mówiąc, moja edycja została odrzucona, ale myślę, że masz na myśli 'float *' not 'int *') –

+0

Zaakceptuję to, jeśli wyjaśnisz, jeśli uważasz, że ostatnim przykładem w moim edytowanym pytaniu jest UB lub nie. (Z technicznego punktu widzenia może po prostu ciekawa, co myślisz.) –

Powiązane problemy