C++
Nieokreślone zachowanie
Szukaj…
Uwagi
Jeśli zachowanie konstrukcji nie jest określone, wówczas standard nakłada pewne ograniczenia na zachowanie, ale pozostawia pewną swobodę implementacji, która nie jest wymagana do udokumentowania tego, co dzieje się w danej sytuacji. Kontrastuje z zachowaniem zdefiniowanym w ramach implementacji, w którym implementacja jest wymagana do udokumentowania tego, co się dzieje, oraz zachowaniem niezdefiniowanym, w którym wszystko może się zdarzyć.
Kolejność inicjalizacji globałów w TU
Podczas gdy w jednostce tłumaczeniowej określony jest porządek inicjalizacji zmiennych globalnych, kolejność inicjalizacji między jednostkami tłumaczeniowymi jest nieokreślona.
Więc program z następującymi plikami
foo.cpp
#include <iostream> int dummyFoo = ((std::cout << "foo"), 0);
bar.cpp
#include <iostream> int dummyBar = ((std::cout << "bar"), 0);
main.cpp
int main() {}
może produkować jako wynik:
foobar
lub
barfoo
Może to prowadzić do statycznego zlecenia inicjalizacji Fiasco .
Wartość wyliczenia poza zakresem
Jeśli wyliczenie o zasięgu zostanie przekonwertowane na typ całkowy, który jest zbyt mały, aby pomieścić jego wartość, wynikowa wartość jest nieokreślona. Przykład:
enum class E {
X = 1,
Y = 1000,
};
// assume 1000 does not fit into a char
char c1 = static_cast<char>(E::X); // c1 is 1
char c2 = static_cast<char>(E::Y); // c2 has an unspecified value
Ponadto, jeśli liczba całkowita jest konwertowana na wyliczenie, a wartość liczby całkowitej jest poza zakresem wartości wyliczenia, wynikowa wartość jest nieokreślona. Przykład:
enum Color {
RED = 1,
GREEN = 2,
BLUE = 3,
};
Color c = static_cast<Color>(4);
Jednak w następnym przykładzie zachowanie nie jest nieokreślone, ponieważ wartość źródłowa mieści się w zakresie wyliczenia, chociaż jest nierówna dla wszystkich podmiotów wyliczających:
enum Scale {
ONE = 1,
TWO = 2,
FOUR = 4,
};
Scale s = static_cast<Scale>(3);
Tutaj s
będzie miało wartość 3 i będzie nierówne w ONE
, TWO
i FOUR
.
Odlewanie statyczne z fałszywej wartości void *
Jeśli wartość void*
jest konwertowana na wskaźnik do typu obiektu T*
, ale nie jest odpowiednio wyrównana dla T
, wynikowa wartość wskaźnika jest nieokreślona. Przykład:
// Suppose that alignof(int) is 4
int x = 42;
void* p1 = &x;
// Do some pointer arithmetic...
void* p2 = static_cast<char*>(p1) + 2;
int* p3 = static_cast<int*>(p2);
Wartość p3
jest nieokreślona, ponieważ p2
nie może wskazywać na obiekt typu int
; jego wartość nie jest właściwie wyrównanym adresem.
Wynik niektórych konwersji reinterpretacji
Wynik reinterpret_cast
z jednego typu wskaźnika funkcji na inny lub jednego odwołania do funkcji na inny jest nieokreślony. Przykład:
int f();
auto fp = reinterpret_cast<int(*)(int)>(&f); // fp has unspecified value
Wynik reinterpret_cast
z jednego typu wskaźnika obiektu na inny lub jednego typu odwołania do obiektu na inny jest nieokreślony. Przykład:
int x = 42;
char* p = reinterpret_cast<char*>(&x); // p has unspecified value
Jednak w przypadku większości kompilatorów było to równoważne z static_cast<char*>(static_cast<void*>(&x))
więc wynikowy wskaźnik p
wskazywał na pierwszy bajt x
. To stało się standardowym zachowaniem w C ++ 11. Zobacz konwersję typu punning, aby uzyskać więcej informacji.
Wynik niektórych porównań wskaźnika
Jeśli dwa wskaźniki są porównywane za pomocą <
, >
, <=
lub >=
, wynik nie jest określony w następujących przypadkach:
Wskaźniki wskazują różne tablice. (Obiekt niebędący tablicą jest uważany za tablicę o rozmiarze 1.)
int x; int y; const bool b1 = &x < &y; // unspecified int a[10]; const bool b2 = &a[0] < &a[1]; // true const bool b3 = &a[0] < &x; // unspecified const bool b4 = (a + 9) < (a + 10); // true // note: a+10 points past the end of the array
Wskaźniki wskazują na ten sam obiekt, ale na elementy o różnej kontroli dostępu.
class A { public: int x; int y; bool f1() { return &x < &y; } // true; x comes before y bool f2() { return &x < &z; } // unspecified private: int z; };
Przestrzeń zajmowana przez odniesienie
Odwołanie nie jest obiektem i, w przeciwieństwie do obiektu, nie ma gwarancji, że zajmie jakieś ciągłe bajty pamięci. Standard pozostawia nieokreślone, czy odniesienie w ogóle wymaga przechowywania. Wiele funkcji języka konspiruje, aby uniemożliwić przenośne sprawdzenie pamięci, którą może zajmować odwołanie:
- Zastosowanie
sizeof
do referencji powoduje zwrócenie rozmiaru referencyjnego typu, co nie daje informacji o tym, czy referencja zajmuje jakąkolwiek pamięć. - Tablice referencji są nielegalne, dlatego nie można badać adresów dwóch kolejnych elementów hipotetycznego odwołania tablic w celu ustalenia rozmiaru referencji.
- Jeśli zostanie wzięty adres odwołania, wynikiem jest adres odwołania, więc nie możemy uzyskać wskaźnika do samego odwołania.
- Jeśli klasa ma element referencyjny, próba wyodrębnienia adresu tego elementu za pomocą
offsetof
daje niezdefiniowane zachowanie, ponieważ taka klasa nie jest klasą o układzie standardowym. - Jeśli klasa ma element referencyjny, klasa nie jest już standardowym układem, więc próby uzyskania dostępu do danych używanych do przechowywania referencji powodują niezdefiniowane lub nieokreślone zachowanie.
W praktyce, w niektórych przypadkach zmienna referencyjna może być implementowana podobnie do zmiennej wskaźnikowej, a zatem zajmuje tyle samo miejsca co wskaźnik, podczas gdy w innych przypadkach referencja może nie zajmować w ogóle miejsca, ponieważ można ją zoptymalizować. Na przykład w:
void f() {
int x;
int& r = x;
// do something with r
}
kompilator może po prostu traktować r
jako alias dla x
i zastąpić wszystkie wystąpienia r
w pozostałej części funkcji f
x
, i nie przydzielać żadnej pamięci do przechowywania r
.
Kolejność oceny argumentów funkcji
Jeśli funkcja ma wiele argumentów, nie jest określone, w jakiej kolejności są obliczane. Poniższy kod może wypisać x = 1, y = 2
lub x = 2, y = 1
ale nie jest określone, w którym.
int f(int x, int y) {
printf("x = %d, y = %d\n", x, y);
}
int get_val() {
static int x = 0;
return ++x;
}
int main() {
f(get_val(), get_val());
}
W C ++ 17 kolejność oceny argumentów funkcji pozostaje nieokreślona.
Jednak każdy argument funkcji jest w pełni oceniany, a obiekt wywołujący jest gwarantowany w ocenie przed argumentami funkcji.
struct from_int {
from_int(int x) { std::cout << "from_int (" << x << ")\n"; }
};
int make_int(int x){ std::cout << "make_int (" << x << ")\n"; return x; }
void foo(from_int a, from_int b) {
}
void bar(from_int a, from_int b) {
}
auto which_func(bool b){
std::cout << b?"foo":"bar" << "\n";
return b?foo:bar;
}
int main(int argc, char const*const* argv) {
which_func( true )( make_int(1), make_int(2) );
}
to musi wydrukować:
bar
make_int(1)
from_int(1)
make_int(2)
from_int(2)
lub
bar
make_int(2)
from_int(2)
make_int(1)
from_int(1)
nie może drukować bar
po którymś z make
lub from
„s, a to nie może wydrukować:
bar
make_int(2)
make_int(1)
from_int(2)
from_int(1)
lub podobne. Przed C ++ 17 bar
drukowania po make_int
s był legalny, podobnie jak robienie obu make_int
s przed wykonaniem jakiegokolwiek z from_int
s.
Przeniesiono ze stanu większości standardowych klas bibliotek
Wszystkie standardowe kontenery biblioteczne po przeniesieniu pozostają w prawidłowym, ale nieokreślonym stanie. Na przykład w poniższym kodzie v2
będzie zawierać {1, 2, 3, 4}
po przeniesieniu, ale nie gwarantuje się, że v1
będzie pusta.
int main() {
std::vector<int> v1{1, 2, 3, 4};
std::vector<int> v2 = std::move(v1);
}
Niektóre klasy mają dokładnie zdefiniowany stan przeniesienia. Najważniejszym przypadkiem jest przypadek std::unique_ptr<T>
, który gwarantuje przeniesienie z null po przeniesieniu.