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

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

26 отзывов на запись «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
wh0cd735078 CIALIS ONLINE
wh0cd548294 misoprostol
wh0cd49942 get the facts
wh0cd90438 Retin-A
wh0cd317718 OVER THE COUNTER VIAGRA
wh0cd49942 viagra
wh0cd90438 buy tadalafil online
wh0cd49942 tadalafil pharmacy
wh0cd90438 cialis online
wh0cd90438 Tadalafil Tablets
wh0cd90438 tadalafil
wh0cd90438 Tadalafil Mail Order
Thanks for finally talking about > C++: не ини­циа­ли­зи­руй­те объ­ек­ты сим­во­лом “=” | Image Processing
< Liked it!
wh0cd331985 neurontin
wh0cd283634 prednisone
After checking out a few of the articles on your website, I seriously appreciate your technique of writing a
blog. I saved it to my bookmark webpage list and will be checking back soon.
Please visit my website as well and tell me what you think.
wh0cd302022 viagra online
Awesome! Its truly amazing post, I have got much clear idea regarding from this
post.

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

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

   

Можете использовать теги <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>