7.13 — Używanie deklaracji i używanie dyrektyw

Prawdopodobnie widziałeś ten program w wielu podręcznikach i tutorialach:

#include <iostream>

using namespace std;

int main()
{
    cout << "Hello world!\n";

    return 0;
}

Jeśli to zobaczysz, uciekaj. Twój podręcznik lub samouczek jest prawdopodobnie nieaktualny. W tej lekcji sprawdzimy dlaczego.

Wskazówka

Niektóre IDE będą również automatycznie wypełniać nowe projekty C++ podobnym programem (dzięki czemu możesz skompilować coś od razu, zamiast zaczynać od pustego pliku).

Krótka lekcja historii

Zanim C++ obsługiwał przestrzenie nazw, wszystkie nazwy, które są teraz w std namespace znajdowały się w globalnej przestrzeni nazw. Spowodowało to kolizje nazewnictwa pomiędzy identyfikatorami programów i standardowymi identyfikatorami bibliotek. Programy, które działały w jednej wersji C++, mogą mieć konflikt nazewnictwa z nowszą wersją C++.

W 1995 roku ujednolicono przestrzenie nazw i całą funkcjonalność standardowej biblioteki przeniesiono z globalnej przestrzeni nazw do przestrzeni nazw std. Ta zmiana zepsuła starszy kod, który nadal używał nazw bez std::.

Jak wie każdy, kto pracował z dużą bazą kodu, każda zmiana w bazie kodu (nieważne, jak trywialna) grozi uszkodzeniem programu. Aktualizowanie każdej nazwy, która została teraz przeniesiona do std przestrzeń nazw, w której chcesz użyć std:: prefiks stanowił ogromne ryzyko. Poproszono o rozwiązanie.

Przejdźmy szybko do dnia dzisiejszego — jeśli często korzystasz z biblioteki standardowej, napisz std:: zanim wszystko, czego użyjesz ze standardowej biblioteki, może stać się powtarzalne, a w niektórych przypadkach może sprawić, że Twój kod będzie trudniejszy do odczytania.

C++ zapewnia pewne rozwiązania obu tych problemów w formie instrukcji użycia.

Ale najpierw zdefiniujmy dwa terminy.

Nazwy kwalifikowane i niekwalifikowane

Nazwa może być kwalifikowana lub niekwalifikowana.

A kwalifikowana nazwa to nazwa zawierająca powiązany zakres. Najczęściej nazwy są kwalifikowane przestrzenią nazw przy użyciu operatora rozpoznawania zakresu (::). Na przykład:

std::cout // identifier cout is qualified by namespace std
::foo // identifier foo is qualified by the global namespace

Dla zaawansowanych czytelników

Nazwę można również kwalifikować za pomocą nazwy klasy za pomocą operatora rozpoznawania zakresu (::) lub obiektu klasy za pomocą operatorów wyboru elementów członkowskich (. lub ->). Na przykład:

class C; // some class

C::s_member; // s_member is qualified by class C
obj.x; // x is qualified by class object obj
ptr->y; // y is qualified by pointer to class object ptr

An niekwalifikowana nazwa to nazwa, która nie zawiera kwalifikatora zakresu. Na przykład, cout i x są nazwami niekwalifikowanymi, ponieważ nie obejmują powiązanego zakresu.

Deklaracje użycia

Jednym ze sposobów ograniczenia liczby powtórzeń pisania std:: w kółko polega na używaniu instrukcji deklaracji użycia. A za pomocą deklaracji pozwala nam używać nazwy niekwalifikowanej (bez zakresu) jako aliasu nazwy kwalifikowanej.

Oto nasz podstawowy program Hello World, wykorzystujący deklarację using w linii 5:

#include <iostream>

int main()
{
   using std::cout; // this using declaration tells the compiler that cout should resolve to std::cout
   cout << "Hello world!\n"; // so no std:: prefix is needed here!

   return 0;
} // the using declaration expires at the end of the current scope

Deklaracja użycia using std::cout; informuje kompilator, że będziemy używać obiektu cout z deklaracji i definicji funkcji std przestrzeń nazw. Więc kiedy tylko to zobaczy cout, założy, że mamy na myśli std::cout. Jeśli istnieje konflikt nazewnictwa pomiędzy std::cout i inne zastosowanie cout to widać od środka main(), std::cout będzie preferowane. Dlatego w linii 6 możemy wpisać cout zamiast std::cout.

Nie oszczędza to wiele wysiłku w tym trywialnym przykładzie, ale jeśli używasz cout wiele razy wewnątrz funkcji deklaracja użycia może sprawić, że kod będzie bardziej czytelny. Pamiętaj, że będziesz potrzebować osobnej deklaracji użycia dla każdej nazwy (np std::cout, jeden za std::cin itd.).

Deklaracja użycia jest aktywna od momentu deklaracji do końca zakresu, w którym została zadeklarowana.

Chociaż deklaracje using są mniej wyraźne niż użycie deklaracji std:: prefiksem, są one ogólnie uważane za bezpieczne i dopuszczalne do użycia w plikach źródłowych (.cpp), z jednym wyjątkiem, który omówimy poniżej.

Dyrektywy używające

Innym sposobem uproszczenia rzeczy jest użycie dyrektywy using. A za pomocą dyrektywy pozwala uniknąć identyfikatory w danej przestrzeni nazw, do których można się odwoływać bez zastrzeżeń z zakresu dyrektywy using.

Dla zaawansowanych czytelników

Z przyczyn technicznych dyrektywy using w rzeczywistości nie wprowadzają nowych znaczeń nazw do bieżącego zakresu - zamiast tego wprowadzają nowe znaczenia nazw do zakresu zewnętrznego (więcej szczegółów na temat wybranego zakresu zewnętrznego można znaleźć tutaj).

Oto ponownie nasz program Hello world z instrukcją użycia w linii 5:

#include <iostream>

int main()
{
   using namespace std; // all names from std namespace now accessible without qualification
   cout << "Hello world!\n"; // so no std:: prefix is needed here

   return 0;
} // the using-directive ends at the end of the current scope

Dyrektywa dotycząca używania using namespace std; informuje kompilator, że wszystkie nazwy w pliku std przestrzeń nazw powinna być dostępna bez kwalifikacji w bieżącym zakresie (w tym przypadku funkcji main()). Kiedy wówczas użyjemy niekwalifikowanego identyfikatora cout, rozwiąże std::cout.

Dyrektywy using to rozwiązanie, które zostało dostarczone dla starych baz kodowych poprzedzających przestrzeń nazw, które używały niekwalifikowanych nazw dla standardowej funkcjonalności bibliotek. Zamiast konieczności ręcznego aktualizowania każdej niekwalifikowanej nazwy do nazwy kwalifikowanej (co było ryzykowne), pojedyncza dyrektywa using (using namespace std;) można umieścić na górze każdego pliku, a wszystkie nazwy, które zostały przeniesione do pliku std przestrzeni nazw można nadal używać bez zastrzeżeń.

Problemy z dyrektywami using (czyli dlaczego należy unikać „używania przestrzeni nazw std;”)

We współczesnym C++ dyrektywy using generalnie oferują niewielkie korzyści (oszczędzają trochę pisania) w porównaniu do ryzyka. Wynika to z dwóch czynników:

  1. Dyrektywy using umożliwiają niekwalifikowany dostęp do uniknąć nazw z przestrzeni nazw (potencjalnie włączając wiele nazw, których nigdy nie użyjesz).
  2. Dyrektywy using nie preferują nazw z przestrzeni nazw zidentyfikowanej przez dyrektywę using w stosunku do innych nazw.

Efektem końcowym jest to, że możliwość wystąpienia kolizji nazw znacznie wzrasta (szczególnie jeśli zaimportujesz plik std przestrzeni nazw).

Najpierw przyjrzyjmy się ilustrującemu przykładowi, w którym dyrektywy using powodują kolizję nazewnictwa:

#include <iostream>

namespace A
{
	int x { 10 };
}

namespace B
{
	int x{ 20 };
}

int main()
{
	using namespace A;
	using namespace B;

	std::cout << x << '\n';

	return 0;
}

W powyższym przykładzie kompilator nie jest w stanie określić, czy plik x w main odnosi się A::x lub B::x. W takim przypadku kompilacja nie powiedzie się i wyświetli się błąd „niejednoznaczny symbol”. Moglibyśmy rozwiązać ten problem, usuwając jedną z dyrektyw using, stosując zamiast niej deklarację using lub kwalifikując x (Jak A::x lub B::x).

Oto kolejny, bardziej subtelny przykład:

#include <iostream> // imports the declaration of std::cout

int cout() // declares our own "cout" function
{
    return 5;
}

int main()
{
    using namespace std; // makes std::cout accessible as "cout"
    cout << "Hello, world!\n"; // uh oh!  Which cout do we want here?  The one in the std namespace or the one we defined above?

    return 0;
}

W tym przykładzie kompilator nie jest w stanie określić, czy nasze nieuprawnione użycie cout oznacza std::cout lub cout zdefiniowaną przez nas funkcję i ponownie nie uda się jej skompilować z powodu błędu „niejednoznacznego symbolu”. Chociaż ten przykład jest trywialny, gdybyśmy wyraźnie przedrostkiem std::cout w ten sposób:

    std::cout << "Hello, world!\n"; // tell the compiler we mean std::cout

lub użył deklaracji użycia zamiast dyrektywy użycia:

    using std::cout; // tell the compiler that cout means std::cout
    cout << "Hello, world!\n"; // so this means std::cout

wtedy nasz program nie miałby żadnych problemów. I chociaż prawdopodobnie nie napiszesz funkcji o nazwie „cout”, w pliku znajdują się setki innych nazw std namespace tylko czeka, aby zderzyć się z twoimi imionami.

Nawet jeśli dyrektywa using nie powoduje dzisiaj kolizji nazewnictwa, sprawia, że ​​Twój kod jest bardziej podatny na przyszłe kolizje. Na przykład, jeśli Twój kod zawiera dyrektywę using dla jakiejś biblioteki, która jest następnie aktualizowana, wszystkie nowe nazwy wprowadzone w zaktualizowanej bibliotece są teraz kandydatami do kolizji nazw z istniejącym kodem.

Na przykład następujący program kompiluje się i działa poprawnie:

FooLib.h (część biblioteki innej firmy):

#ifndef FOOLIB
#define FOOLIB

namespace Foo
{
    int a { 20 };
}

#endif

main.cpp:

#include <iostream>
#include <FooLib.h> // a third-party library we installed outside our project directory, thus angled brackets used

void print()
{
    std::cout << "Hello\n";
}

int main()
{
    using namespace Foo; // Because we're lazy and want to access Foo:: qualified names without typing the Foo:: prefix

    std::cout << a << '\n'; // uses Foo::a
    print(); // calls ::print()

    return 0;
}

Załóżmy teraz, że aktualizujesz FooLib do nowej wersji, a FooLib.h zmienia się w następujący sposób:

FooLib.h (zaktualizowano):

#ifndef FOOLIB
#define FOOLIB

namespace Foo
{
    int a { 20 };
    void print() { std::cout << "Timmah!"; } // This function added
}
#endif

Twój plik main.cpp nie uległ zmianie, ale nie będzie się już kompilować! Dzieje się tak dlatego, że nasze przyczyny używają-dyrektywy Foo::print() być dostępnym tak samo print()i obecnie nie jest jasne, czy wywołanie print() oznacza ::print() lub Foo::print().

Istnieje bardziej podstępna wersja tego problemu, która może również wystąpić. Zaktualizowana biblioteka może wprowadzić funkcję, która nie tylko ma tę samą nazwę, ale w rzeczywistości lepiej pasuje do niektórych wywołań funkcji. W takim przypadku kompilator może zdecydować się na preferowanie nowej funkcji, a zachowanie programu zmieni się nieoczekiwanie i po cichu.

Rozważ następujący program:

Foolib.h (część jakiejś biblioteki innej firmy):

#ifndef FOOLIB_H
#define FOOLIB_H

namespace Foo
{
    int a { 20 };
}
#endif

main.cpp:

#include <iostream>
#include <Foolib.h> // a third-party library we installed outside our project directory, thus angled brackets used

int get(long)
{
    return 1;
}

int main()
{
    using namespace Foo; // Because we're lazy and want to access Foo:: qualified names without typing the Foo:: prefix
    std::cout << a << '\n'; // uses Foo::a

    std::cout << get(0) << '\n'; // calls ::get(long)

    return 0;
}

Ten program działa i drukuje 1.

Załóżmy teraz, że aktualizujemy bibliotekę Foolib, która zawiera zaktualizowany Foolib.h, który wygląda tak:

Foolib.h (zaktualizowano):

#ifndef FOOLIB_H
#define FOOLIB_H

namespace Foo
{
    int a { 20 };

    int get(int) { return 2; } // new function added
}
#endif

Po raz kolejny nasz main.cpp plik w ogóle się nie zmienił, ale ten program teraz kompiluje, uruchamia i wypisuje 2!

Kiedy kompilator napotyka wywołanie funkcji, musi określić, z jaką definicją funkcji powinien pasować do wywołania funkcji. Wybierając funkcję ze zbioru potencjalnie pasujących funkcji, będzie preferować funkcję, która nie wymaga konwersji argumentów, zamiast funkcji wymagającej konwersji argumentów. Ponieważ literał 0 jest liczbą całkowitą, C++ będzie wolał dopasować print(0) z nowo wprowadzonym print(int) (bez konwersji) zamiast print(long) (co wymaga konwersji z int Do long). To powoduje nieoczekiwaną zmianę w zachowaniu naszego programu.

W tym przypadku zmiana w zachowaniu jest dość oczywista. Jednak w bardziej złożonym programie, w którym zwracana wartość nie jest tylko drukowana, wykrycie tego problemu może być bardzo trudne.

Nie doszłoby do tego, gdybyśmy użyli deklaracji użycia lub jawnego kwalifikatora zakresu.

W końcu brak wyraźnych przedrostków zakresu utrudnia czytelnikowi określenie, które funkcje są częścią biblioteki, a które częścią twojego programu. Na przykład, jeśli użyjemy dyrektywy using:

using namespace NS;

int main()
{
    foo(); // is this foo a user-defined function, or part of the NS library?
}

Nie jest jasne, czy wywołanie foo() jest w rzeczywistości wywołaniem NS::foo() lub a foo() , czyli funkcją zdefiniowaną przez użytkownika. Nowoczesne IDE powinny być w stanie to rozróżnić, gdy najedziesz kursorem na nazwę, ale konieczność najechania myszką na każdą nazwę, aby zobaczyć, skąd pochodzi, jest nudna.

Bez dyrektywy using jest to znacznie jaśniejsze:

int main()
{
    NS::foo(); // clearly part of the NS library
    foo(); // likely a user-defined function
}

W tej wersji wywołanie NS::foo() jest wyraźnie wywołaniem bibliotecznym. Wywołanie zwykły foo() jest prawdopodobnie wywołaniem funkcji zdefiniowanej przez użytkownika (niektóre biblioteki, w tym niektóre standardowe nagłówki bibliotek, umieszczają nazwy w globalnej przestrzeni nazw, więc nie jest to gwarancją).

Zakres instrukcji using

Jeśli w bloku używana jest deklaracja użycia lub dyrektywa użycia, nazwy mają zastosowanie tylko do tego bloku (zgodnie z normalnymi regułami określania zakresu bloków). Jest to dobra rzecz, ponieważ zmniejsza ryzyko wystąpienia kolizji nazw tylko w obrębie tego bloku.

Jeśli w przestrzeni nazw (w tym globalnej przestrzeni nazw) używana jest deklaracja using lub dyrektywa using, nazwy mają zastosowanie do całej reszty pliku (mają zakres pliku).

Nie używaj instrukcji using w plikach nagłówkowych lub przed dyrektywą #include

Dobra zasada Należy pamiętać, że instrukcji using nie należy umieszczać nigdzie, gdzie mogłyby mieć wpływ na kod w innym pliku. Nie należy ich też umieszczać w miejscu, gdzie kod innego pliku mógłby mieć na nie wpływ.

A dokładniej oznacza to, że instrukcji using nie należy używać w plikach nagłówkowych ani przed dyrektywą #include.

Na przykład, jeśli umieścisz instrukcję using w globalnej przestrzeni nazw pliku nagłówkowego, wówczas każdy inny plik, który #included będzie zawierał ten nagłówek, również otrzyma tę instrukcję using. To wyraźnie złe. Z tego samego powodu dotyczy to również przestrzeni nazw wewnątrz plików nagłówkowych.

Ale co z instrukcjami using w funkcjach zdefiniowanych w plikach nagłówkowych? Z pewnością nie może to być złe, ponieważ zakres instrukcji using jest zawarty w funkcji, prawda? Nawet to nie. I jest to „nie” z tego samego powodu, dla którego nie powinniśmy używać instrukcji using przed dyrektywą #include.

Okazuje się, że zachowanie instrukcji using zależy od tego, jakie identyfikatory zostały już wprowadzone. To czyni je zależnymi od kolejności, gdyż ich funkcja może ulec zmianie, jeśli zmienią się identyfikatory wprowadzone przed nimi.

Zilustrujemy to na przykładzie:

FooInt.h:

namespace Foo
{
    void print(int)
    {
        std::cout << "print(int)\n" << std::endl;
    }
}

FooDouble.h:

namespace Foo
{
    void print(double)
    {
        std::cout << "print(double)\n" << std::endl;
    }
}

main.cpp (okay):

#include <iostream>

#include "FooDouble.h"
#include "FooInt.h"

using Foo::print; // print means Foo::print

int main()
{
    print(5);  // Calls Foo::print(int)
}

Po uruchomieniu program ten wywołuje Foo::print(int), która drukuje print(int).

Teraz zmieńmy się main.cpp nieznacznie.

main.cpp (zły):

#include <iostream>

#include "FooDouble.h"

using Foo::print; // we moved the using-statement here, before the #include directive
#include "FooInt.h"

int main()
{
    print(5);  // Calls Foo::print(double)
}

Wszystko, co zrobiliśmy, to przenieś using Foo::print; przed #include "FooInt.h". A nasz program teraz drukuje print(double)! Niezależnie od tego, dlaczego tak się dzieje, prawdopodobnie zgodzisz się, że jest to rodzaj zachowania, którego chcemy uniknąć!

A więc wróćmy do tematu. Powód, dla którego nie powinniśmy używać instrukcji using w funkcjach zdefiniowanych w plikach nagłówkowych, jest z tego samego powodu — nie możemy kontrolować, które inne nagłówki mogą być #uwzględnione przed naszym nagłówkiem i możliwe jest, że nagłówki te mogą zrobić coś, co zmieni sposób naszej instrukcji using zachowuje się!

Jedyne miejsce, w którym naprawdę bezpieczne jest używanie instrukcji using, znajdują się w naszych plikach źródłowych (.cpp), po wszystkich #includes.

Dla zaawansowanych czytelników

W tym przykładzie zastosowano koncepcję, której jeszcze nie omawialiśmy, zwaną „przeciążaniem funkcji” (omówimy to w lekcji 11.1 -- Wprowadzenie do funkcji przeciążenie). Wszystko, co musisz wiedzieć w tym przykładzie, to to, że dwie funkcje w tym samym zakresie mogą mieć tę samą nazwę, o ile parametry każdej z nich są różne. Ponieważ int i double są odrębnymi typami, nie ma problemu, aby oba Foo::print(int) i Foo::print(double) żyły obok siebie.

W wersji roboczej, gdy kompilator napotyka using Foo::print, widział już oba Foo::print(int) i Foo::print(double), więc umożliwia wywołanie obu jako just print(). Ponieważ Foo::print(int) jest lepszym dopasowaniem niż Foo::print(double), wywołuje Foo::print(int).

W złym wersji, gdy kompilator napotka using Foo::print, widział tylko deklarację dla Foo::print(double), więc udostępnia tylko Foo::print(double) niekwalifikowany. Więc kiedy zadzwonimy, print(5) tylko Foo::print(double) może nawet zostać dopasowany. Zatem Foo::print(double) jest wywoływany!

Anulowanie lub zastępowanie instrukcji using

Po zadeklarowaniu instrukcji using nie ma możliwości jej anulowania ani zastąpienia inną instrukcją using w zakresie, w jakim została zadeklarowana.

int main()
{
    using namespace Foo;

    // there's no way to cancel the "using namespace Foo" here!
    // there's also no way to replace "using namespace Foo" with a different using statement

    return 0;
} // using namespace Foo ends here

Najlepsze, co możesz zrobić, to celowo ograniczyć zakres instrukcji using od samego początku za pomocą bloku reguły ustalania zakresu.

int main()
{
    {
        using namespace Foo;
        // calls to Foo:: stuff here
    } // using namespace Foo expires
 
    {
        using namespace Goo;
        // calls to Goo:: stuff here
    } // using namespace Goo expires

    return 0;
}

Oczywiście całego tego bólu głowy można uniknąć, jawnie używając w pierwszej kolejności operatora rozpoznawania zakresu (::).

Dobre praktyki dotyczące instrukcji using

Najlepsza praktyka

Preferuj jawne kwalifikatory przestrzeni nazw zamiast instrukcji using.

Unikaj całkowicie dyrektyw using (z wyjątkiem using namespace std::literals aby uzyskać dostęp do s i sv dosłownego przyrostki). Deklaracje using są w porządku w plikach .cpp, po dyrektywach #include. Nie używaj instrukcji using w plikach nagłówkowych (szczególnie w globalnej przestrzeni nazw plików nagłówkowych).

Powiązana treść

Klasa using słowo kluczowe jest również używane do definiowania aliasów typów, które nie są powiązane z instrukcjami using. Aliasy typów omówimy w lekcji 10.7 -- Typedefs i aliasy typów.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
243 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze