C++: не ини­циа­ли­зи­руй­те объ­ек­ты сим­во­лом “=”

Всё ска­зан­ное здесь не от­но­сит­ся к Visual Studio. В этой си­сте­ме, во­пре­ки стан­дар­там, ини­циа­ли­за­ция ра­вен­ством эк­ви­ва­лент­на ини­циа­ли­за­ции скоб­ка­ми (кро­ме слу­чая explicit-кон­ст­рук­то­ров).

Ча­сто мож­но встре­тить код, в ко­то­ром кон­струи­руе­мый объ­ект ини­циа­ли­зи­ру­ет­ся сим­во­лом «=»:

MyClass x = 10;

Та­кая за­пись на­зы­ва­ет­ся ини­циа­ли­за­ци­ей ко­пии (copy-initialization).

На сай­те govnokod.ru к этой ста­тье от­нес­лись до­ста­точ­но не­га­тив­но (ци­ти­рую: «ав­тор еба­нул­ся»). В оправ­да­ние ска­жу, что с тех пор ста­тья бы­ла силь­но до­ра­бо­та­на.

При­выч­ка ис­поль­зо­вать ини­циа­ли­за­цию ко­пии по­шла, ви­ди­мо, из «сиш­ных» вре­мён, ко­гда клас­сов и кон­ст­рук­то­ров не бы­ло, а ука­зан­ная за­пись бы­ла един­ствен­ным спо­со­бом ини­циа­ли­за­ции. Вме­сто это­го сле­ду­ет, где толь­ко мож­но, ис­поль­зо­вать ва­ри­ант со скоб­ка­ми:

MyClass x(10);

Эта за­пись на­зы­ва­ет­ся пря­мой ини­циа­ли­за­ци­ей (direct-initialization).

Да­вай­те раз­бе­рём­ся со все­ми «за» и «про­тив».

1. Ини­циа­ли­за­ция ко­пии в об­щем слу­чае ме­нее эф­фек­тив­на, чем пря­мая ини­циа­ли­за­ция

User-defined conversion sequen­ces that can convert from the source type to the destination type or (when a conversion function is used) to a derived class are enumerated, and the best one is chosen through overload resolution. If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the fun­ction is a constructor, the call initiali­zes a temporary of the destination type. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization.

При­чи­на в том, что за­пись MyClass x = 10; эк­ви­ва­лент­на за­пи­си MyClass x( MyClass(10) ); (при этом внут­рен­нее вы­ра­же­ние MyClass(10) не обя­за­тель­но пред­став­ля­ет со­бой вы­зов кон­ст­рук­то­ра; это мо­жет быть так­же и поль­зо­ватель­ская функ­ция пре­об­ра­зо­ва­ния ти­па). Спра­ва при­ве­де­на вы­держ­ка из стан­дар­та C++ 2003, опи­сы­ваю­щая по­ве­де­ние ком­пи­ля­то­ра в этом слу­чае.

Дру­ги­ми сло­ва­ми, ис­поль­зуя знак ра­вен­ства вы вы­би­ра­е­те имен­но кон­ст­рук­тор ко­пии, а для не­го ком­пи­ля­тор уже пы­та­ет­ся по­до­брать под­хо­дя­щее пре­об­ра­зо­ва­ние ти­па. По­это­му та­кая ини­циа­ли­за­ция мо­жет при­ве­сти к со­зда­нию про­ме­жу­точ­ной ко­пии объ­ек­та да­же при на­ли­чии под­хо­дя­ще­го кон­ст­рук­то­ра.

Пря­мая же ини­циа­ли­за­ция (ини­циа­ли­за­ция скоб­ка­ми) поз­во­ля­ет ком­пи­ля­то­ру вы­брать под­хо­дя­щий кон­ст­рук­тор из тех, что опре­де­лил раз­ра­бот­чик клас­са.

Рас­смот­рим при­мер. До­пу­стим, име­ет­ся класс, ко­то­рый мо­жет быть ини­циа­ли­зи­ро­ван как объ­ек­том это­го же клас­са, так и це­лым чис­лом:

#include <iostream>

class MyInt
{
private:
    int x;
public:
    MyInt(int const y): x( y )
    { std::cout << "Constructor 1 called." << std::endl; }
    MyInt(MyInt const &y): x( y.x )
    { std::cout << "Constructor 2 called." << std::endl; }
};

int main(void)
{
    MyInt i = 1;

    return 0;
}

В этом при­ме­ре раз­ра­бот­чик клас­са сде­лал кон­ст­рук­тор для со­зда­ния эк­зем­пля­ра клас­са из встро­ен­но­го ти­па int, од­на­ко не­ра­ди­вый поль­зо­ватель клас­са бло­ки­ру­ет воз­мож­ность пря­мо­го кон­струи­ро­ва­ния. Вот, что мы по­лу­чим, за­пу­стив этот код:

Constructor 1 called.
Constructor 2 called.

От­ра­бо­та­ли оба кон­ст­рук­то­ра. Пер­вый — для пре­об­ра­зо­ва­ния ти­па int в тип MyInt, вто­рой — для со­зда­ния нуж­но­го нам объ­ек­та MyInt i из пре­об­ра­зо­ван­но­го.

Не­мно­го из­ме­ним код (те­ло клас­са та­кое же, как в преды­ду­щем при­ме­ре):

int main(void)
{
    MyInt i(1);

    return 0;
}

При за­пус­ке име­ем бо­лее ожи­дае­мый ре­зуль­тат:

Constructor 1 called.

Од­на­ко не всё так пло­хо. Стан­дарт C++ 2003 раз­ре­ша­ет ком­пи­ля­то­рам в не­ко­то­рых слу­ча­ях опус­кать вы­зов кон­ст­рук­то­ра ко­пии:

  • In a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object with the same cv-unqualified type as the function return type, the copy operation can be omitted by constructing the automatic object directly into the function’s return value.

  • When a temporary class object that has not been bound to a reference would be copied to a class object with the same cv-unqualified type, the copy operation can be omitted by constructing the temporary object directly into the target of the omitted copy.

Вто­рой пункт — как раз наш слу­чай. На­при­мер, в ком­пи­ля­то­ре GCC ин­тел­лект по ис­клю­че­нию вы­зо­вов лиш­них кон­ст­рук­то­ров ак­ти­ви­ру­ет­ся клю­чём -felide-constructors, а дезак­ти­ви­ру­ет­ся клю­чём -fno-elide-constructors.

Но да­же опу­щен­ный вы­зов кон­ст­рук­то­ра ко­пии не поз­во­лит нам от­ком­пи­ли­ро­вать про­грам­му, ес­ли кон­ст­рук­тор ко­пии скрыт (private):

#include <iostream>

class MyInt
{
private:
    int x;
    MyInt(MyInt const &y): x( y.x )
    { std::cout << "Constructor 2 called." << std::endl; }
public:
    MyInt(int const y): x( y )
    { std::cout << "Constructor 1 called." << std::endl; }
};

int main(void)
{
    MyInt i = 1; //Ошибка: конструктор копии скрыт

    return 0;
}

На govnokod.ru под­ска­зы­ва­ют, что это по­ве­де­ние ком­пи­ля­то­ра Visual Studio ис­прав­ля­ет­ся клю­чом /Za (Disable Language Extensions).

Код вы­ше — как раз тот слу­чай, ко­гда про­яв­ля­ет­ся без­раз­ли­чие ком­пи­ля­то­ра Visual Studio к спо­со­бу ини­циа­ли­за­ции; в Сту­дии при­мер от­ком­пи­ли­ру­ет­ся и бу­дет ра­бо­тать, че­го по стан­дар­ту не долж­но быть.

2. Ини­циа­ли­за­цию ко­пии лег­ко спу­тать с опе­ра­ци­ей при­сваи­ва­ния

Пом­ни­те, что чи­таю­щие ваш код мо­гут быть не очень хо­ро­шо зна­ко­мы с C++ и с тру­дом от­ли­чать кон­ст­рук­тор от при­сваи­ва­ния.

Рас­смот­рим при­мер:

std::string      s = "Anton";
int              n = (int) 11.23;
std::vector<int> v = (std::vector<int>) 11;

Для мно­гих лю­дей пер­вая строч­ка при­ве­дён­но­го ко­да зву­чит как «со­здать стро­ку s и при­сво­ить ей зна­че­ние "Anton"». Ошиб­кой та­ко­го объ­яс­не­ния яв­ля­ет­ся пред­по­ло­же­ние о су­ще­ство­ва­нии скон­струи­ро­ван­ной стро­ки на мо­мент за­пол­не­ния её дан­ны­ми. Эта ошиб­ка ста­но­вит­ся фа­таль­ной при по­пыт­ке ана­ло­гич­но по­нять тре­тью строч­ку: по­лу­ча­ет­ся, что мы со­зда­ём мас­сив це­лых чи­сел, а за­тем при­сваи­ва­ем ему чис­ло 11 (на са­мом же де­ле тре­тья строч­ка со­зда­ёт мас­сив из 11-ти ну­лей).

Тут есть ещё од­но зло — ис­поль­зо­ва­ние име­ни ти­па дан­ных в скоб­ках для пре­об­ра­зо­ва­ния ти­па (то­же на­сле­дие сиш­ных вре­мён). Я имею в ви­ду за­пи­си (std::vector<int>) и (int). Об­суж­де­ние этой про­бле­мы, од­на­ко, тре­бу­ет от­дель­ной ста­тьи.

По­это­му не услож­няй­те чте­ние и по­ни­ма­ние ва­шей про­грам­мы — сде­лай­те ис­поль­зо­ва­ние кон­ст­рук­то­ров бо­лее яв­ным при по­мо­щи круг­лых ско­бок:

std::string      s("Anton");
int              x((int)11.23);
std::vector<int> v(11);

Рас­смот­рим ещё при­мер, свя­зан­ный с по­ис­ком ошиб­ки в ко­де. Ко­гда мы про­смат­ри­ва­ем текст про­грам­мы, то за­ча­стую ана­ли­зи­ру­ем её на уров­не «изоб­ра­же­ния» (то, как она вы­гля­дит), а не на уров­не тек­ста. При этом нам бу­дет тя­же­ло най­ти ошиб­ку в сле­дую­щей про­грам­ме:

#include <iostream>

struct MyInt
{
    int x;
    MyInt &operator=(int const y) { x = y; return *this; }    
    MyInt(int const y): x(x) { ; }
    MyInt(MyInt const &y): x(y.x) { ; }
};

int main(void)
{
    MyInt i = 1; //Операция присваивания?

    std::cout << i.x << std::endl; //Выведет 0. Почему?

    i = 1;       //Операция присваивания!

    std::cout << i.x << std::endl; //Выведет 1
    return 0;
}

Про­бле­ма в том, что при пр­осмот­ре те­ла клас­са мы в первую оче­редь об­ра­ща­ем вни­ма­ние на опе­ра­цию при­сваи­ва­ния, так как она по­хо­жа на строч­ку MyInt i = 1;, ко­то­рая по­че­му-то не­вер­но ра­бо­та­ет.

3. Знак ра­вен­ства не вез­де мож­но ис­поль­зо­вать для ини­циа­ли­за­ции

Один из слу­ча­ев мы с ва­ми толь­ко что ви­де­ли: это спи­сок ини­циа­ли­за­то­ров в кон­ст­рук­то­рах клас­са. Ис­поль­зо­вать ра­вен­ство там нель­зя:

MyInt(MyInt const &y): x = y.x { ; } //Так нельзя
MyInt(MyInt const &y): x(y.x) { ; } //Так можно

Вто­рой слу­чай — ис­поль­зо­ва­ние бо­лее од­но­го ар­гу­мен­та для ини­циа­ли­за­ции. Ра­вен­ство поз­во­ля­ет ис­поль­зо­вать ров­но один ар­гу­мент. Пред­ста­вим, что у вас есть класс Fraction, со­зда­вае­мый из чис­ли­те­ля и зна­ме­на­те­ля ти­па int, то­гда:

Fraction f = 2, 3; //Так нельзя
Fraction f(2, 3); //Так можно

Ес­ли скоб­ка­ми ини­циа­ли­зи­ро­вать мож­но все­гда, а ра­вен­ством — лишь ино­гда, то по­че­му бы не ис­поль­зо­вать скоб­ки вез­де?

За­клю­че­ние

На­де­юсь, мне уда­лось пе­ре­убе­дить про­грам­ми­стов, пе­ре­шед­ших на C++ с язы­ка C в том, что ини­циа­ли­за­цию ра­вен­ством нуж­но за­быть, как страш­ный сон (вы ведь уже за­бы­ли про printf() для вы­во­да в кон­соль?)

В за­клю­че­ние до­бав­лю не­боль­шую лож­ку дёг­тя. По стан­дар­ту OpenMP ини­циа­ли­за­ция счёт­чи­ка рас­па­рал­ле­ли­ва­е­мо­го цик­ла долж­на вы­пол­нять­ся имен­но зна­ком ра­вен­ства, а не скоб­ка­ми:

#pragma omp parallel for
for(int i(0); i < N; ++i) //Так нельзя, несмотря на то, что выглядит красиво
  какая_нибудь_функция(i);

#pragma omp parallel for
for(int i = 0; i < N; ++i) //Так можно
  какая_нибудь_функция(i);

for(int i(0); i < N; ++i) //Так можно, так как цикл не распараллеливается
  какая_нибудь_функция(i);

На­де­юсь, этот де­фект вско­ре по­пра­вят.

Восемь отзывов на запись «C++: не ини­циа­ли­зи­руй­те объ­ек­ты сим­во­лом “=”»

gorban@nix:/tmp$ cat test.cpp 
#include <iostream>

class MyInt {
private:
    int x;
public:
    MyInt(int const y): x( y )
    { std::cout < < "Constructor 1 called." << std::endl; }
    MyInt(MyInt const &y): x( y.x )
    { std::cout << "Constructor 2 called." << std::endl; }
};

int main(void)
{
    MyInt i = 1;

    return 0;
}

gorban@nix:/tmp$ g++ test.cpp 
gorban@nix:/tmp$ ./a.out 
Constructor 1 called.
Спа­си­бо, разо­брал­ся. Ис­ти­на по­сре­ди­не. Че­рез па­ру дней об­нов­лю эту ста­тей­ку.
Зд­рас­т­вуй­те) Спа­си­бо боль­шое за ста­тью! Я на­чи­наю­щий в С++, и у ме­ня та­кой (воз­мож­но ту­по­ва­тый) во­прос:
а за­чем про­стые ти­пы ини­циа­ли­зи­ро­вать скоб­ка­ми (нар­пи­мер, int i(10);)? Это же про­стые ти­пы, а не клас­сы. Раз­ве у них есть кон­ст­рук­то­ры?
Для про­стых ти­пов не важ­но, как ини­циа­ли­зи­ро­вать.
buy clomid
where can i get clomid online
sildalis without a prescription
nexium online

Оставить отзыв

Жёлтые поля обязательны к заполнению

   

Можете использовать теги <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang=""> <div class=""> <span class=""> <br>