<
  • Главная
Статьи

Антипаттерн програмування на плюсах

С ++ - справжній чоловічий мову програмування. Програмуючи на ньому, ти легко можеш відстрелити собі ногу, та так, що дізнаєшся про це тільки через добрі півроку. Що ж робити? Читати стандарт! І цю статтю, до речі, теж.

Коли пишеш код на C ++, завжди треба думати, як він буде використовуватися. Від цього залежить, які можливості мови треба застосувати, щоб цей код працював саме так, як задумано. Сі з плюсами рясніє різноманітними ключовими словами, які ніби як не обов'язкові до використання, і багато програмісти їх ігнорують. Ще С ++ дуже любить, щоб кодер пам'ятав і дбав про деталі, без належного рівня уважності баги будуть сипатися один за іншим. Для більшої переконливості наведемо кілька приклад.

Всі знають, що таке в C ++ конструктор і для чого він потрібен. Якщо ініціалізація будь-якого члена в класі лягає на плечі користувача цього класу, то таку операцію можна провести через передачу в конструктор якогось параметра. Давай подивимося на такий клас:

class MyClass {public: MyClass (): m_x (0), m_y (0) {} MyClass (int x): m_x (x), m_y (0) {} void SetY (int y) {m_y = y; } Private: int m_x; int m_y; };

Все просто до неподобства. Є конструктор за замовчуванням і конструктор, не започатковано змінну член m_x значенням x. Ще є m_y, для ініціалізації якого служить функція член SetY. Цей клас можна використовувати наступним чином:

MyClass myObject (7); myObject.SetY (9); // ... myObject = 12;

Здавалося б, код абсолютно нешкідливий. Спочатку ми створюємо об'єкт myObject, ініціалізувавши m_x значенням 7. Після цього присвоюємо m_y значення 9. Через кілька рядків коду ми робимо myObject = 12 ;, і в цьому полягає велика проблема. Так як для класу є конструктор, який приймає як параметр значення типу int, то під час присвоєння компілятор, вирішивши полегшити нам життя, автоматично перетворює 12 в об'єкт типу MyClass і замінить їм myObject. При цьому значення в m_y буде безповоротно втрачено. Якщо програміст не розуміє, що він робить, або просто припустився такої помилки через неуважність, то це загрожує нам великими проблемами. У деяких випадках проблеми можуть стати особливо серйозними. Для многопоточного програмування був придуманий патерн Monitor, який дозволяє захистити будь-який об'єкт м'ютексів, щоб згодом надавати до нього безпечний доступ в багатопотокової середовищі. Код цього патерну схожий на код класу з попереднього прикладу, різниця лише в тому, що замість члена int m_y буде std :: mutex m_mutex, а функція SetYбудет замінена на AccessToX. Для того щоб безпечно працювати з захищеними даними, монітор дозволяє передати в перевантажений operator () лямбда-функцію, яка буде виконуватися під м'ютексів. Тепер уявімо, що буде, якщо в багатопотокової середовищі ми разом з компілятором проведемо такий трюк з неявним перетворенням і подальшим присвоєнням монітора новоствореного тимчасового об'єкта.

Тепер уявімо, що буде, якщо в багатопотокової середовищі ми разом з компілятором проведемо такий трюк з неявним перетворенням і подальшим присвоєнням монітора новоствореного тимчасового об'єкта

Статистичний аналіз коду в Visual Studio

У більшості випадків все відпрацює, здавалося б, як треба, але якщо такий код буде викликаний одночасно з різних потоків, то ми отримаємо всі принади data race. Роблячи myObject = 12 для об'єкта монітора, програміст, скоріше, хотів перевизначити захищається значення, але з якихось причин не скористався спеціально створеним для цього механізмом. Можна, звичайно, сказати йому: «Геть з професії», але насправді винен саме той, хто проектував клас монітора. Щоб подібних проблем не виникало, достатньо лише оголосити конструктор MyClass (int x) з ключовим словом explicit: воно відключить неявне перетворення аргументу в об'єкт, і конструкція myObject = 12 Не скомпілюється, що змусить неуважного кодера задуматися.

Поліморфізм в C ++ використовується по повній. Всі програмісти, коли-небудь стикаються з ООП, знають, що це таке. Дуже зручно, наприклад, зберігати об'єкти різних пов'язаних між собою класів в контейнері, який працює з покажчиками на базовий клас. Завдяки віртуальним функціям виконується саме той код, який нам потрібен. Це надзвичайно зручно і звично, і деякі навіть забувають про те, що доступ до функцій спадкоємця можна отримати тільки через покажчик, але ніяк не через об'єкт базового класу. Щоб було зрозуміліше, поглянемо на наступний код:

class A {public: A (): m_a (0) {std :: cout << "Construct A" << std :: endl; } A (const A & other) {std :: cout << "Copy construct A" << std :: endl; CopyFrom (other); } Virtual ~ A () {} virtual void Foo () {std :: cout << "A :: Foo" << std :: endl; } Private: void CopyFrom (const A & other) {if (this! = & Other) {m_a = other.m_a; }} Private: int m_a; }; class B: public A {public: B (): A () m_b (0) {std :: cout << "Construct B" << std :: endl; } B (const B & other) {std :: cout << "Copy construct B" << std :: endl; A :: CopyFrom (other); CopyFrom (other); } Void Foo () override {std :: cout << "B :: Foo" << std :: endl; } Private: void CopyFrom (const B & other) {if (this! = & Other) {m_b = other.m_b; }} Private: int m_b; };

У нас є базовий клас A і його спадкоємець B. Віртуальна функція Foo перевизначена в дочірньому класі. До того ж ми явно визначили конструктор копіювання в обох класах, але це ми залишимо на потім. Давай уявимо, ніби у нас є вектор, в якому потрібно зберігати об'єкти обох цих типів, але з якихось причин ми вирішили, що в контейнері повинні лежати самі об'єкти, а не покажчики на них. Запущено туди кілька об'єктів, ми проходимо по контейнеру і викликаємо для кожного Foo.

std :: vector <A> myVector; myVector.push_back (B ()); myVector.push_back (B ()); myVector.push_back (A ()); for (auto & object: myVector) {object.Foo (); }

Після виконання циклу ми очікуємо побачити в консольному виведення рядка, що свідчать про виклик двох B :: Foo і однієї A :: Foo, але попри це все три виклики будуть виводити в output A :: Foo. Виходить, поліморфізм не працює? Не зовсім так.

При приміщенні об'єктів в вектор відбувається їх копіювання. Ми спеціально переопределили конструктор копіювання, щоб зрозуміти, що відбувається. А відбувається те, що викликається конструктор копіювання тільки для класу A, так як саме він є шаблонною параметром контейнера. Все, що стосується класу B, при цьому не копіюється, в тому числі і таблиця покажчиків на віртуальні функції. Таким чином, у векторі лежать звичайні об'єкти класу A. Така помилка можлива як через незнання основ, так і по простій неуважності. Найнебезпечніше в цій ситуації полягає в тому, що розплата може прийти через багато днів і шукати причину глюків буде складно.

Досить часто програмісти стикаються із завданням виклику клієнтського коду зі свого. Це можуть бути прості callbacks або реалізація сигналів / слотів. Що не мають досвіду в подібних речах програмісти наївно сподіваються, що їх класами будуть користуватися ідеальні кодери, які не припускаються помилок. Творіння подібних программеров виглядає приблизно так:

void SomeClass :: Foo (std :: function <void ()> callback) {m_mutex.lock () // Робимо що-небудь добре callback (); m_mutex.unlock (); }

Для більшої драматичності ми спеціально додали м'ютекс в наш код. Відчув весь трагізм того, що відбувається? Справа в тому, що при виклику callback () цілком може статися виняток, яке передчасно призведе до завершення функції SomeClass :: Foo, і значить, м'ютекс, який ми заблокували на початку цієї функції, залишиться в такому стані назавжди, що згодом неминуче призведе до Дедлок.

Справа в тому, що при виклику callback () цілком може статися виняток, яке передчасно призведе до завершення функції SomeClass :: Foo, і значить, м'ютекс, який ми заблокували на початку цієї функції, залишиться в такому стані назавжди, що згодом неминуче призведе до Дедлок

Шматочок коду сигналів / слотів

Виправити становище справ досить просто - достатньо лише використовувати керуючий об'єкт для нашого мьютекса, в даному випадку цілком підійде std :: lock_guard. Стандарт C ++ гарантує, що локальні об'єкти в разі спрацювання exception будуть знищені, а отже, будуть викликані їх деструктори, деструктор lock_guard, в свою чергу, розблокує м'ютекс. Можливий і інший варіант розвитку подій. Припустимо, наш коллбек потрібно викликати не один раз, а кілька разів в циклі. В цьому випадку ми цілком можемо розраховувати на дострокове завершення нашого коду через спрацювання все того ж виключення в callback. На щастя, і тут нескладно впоратися з випали на нашу долю труднощами.

void SomeClass :: Foo (std :: function <void ()> callback) {while (/ * поки я живий * /) {// Працюємо на благо людства try {callback (); } Catch (...) {// Обробляємо виключення тут! }}}

Обернувши виклик користувальницького коду в try-catch блок, ми зможемо без побоювань молотити біти і байти в нашому циклі, не боячись, що який-небудь кодер-самоучка захоче поділити все це на нуль. З мого великого і досить-таки очевидного розповіді треба засвоїти дві речі: завжди використовуй керуючі (RIIA) об'єкти для всіх ресурсів, які вимагають деініціалізацію, і відловлюють виключення при викликах клієнтського коду, якщо не хочеш неприємних сюрпризів.

Відстрелити собі ногу, займаючись кодінгом на плюсах, ще простіше, ніж проколупнути дірку в паперовій стіні японського будинку. Слід пам'ятати безліч нюансів і проявляти особливу пильність. Щоб не робити прикрих помилок, потрібно багато досвіду. А для профілактики можна періодично перегортати стандарт C ++.

Що ж робити?
Виходить, поліморфізм не працює?
Відчув весь трагізм того, що відбувається?


Новости
  • Виртуальный хостинг

    Виртуальный хостинг. Возможности сервера распределяются в равной мере между всеми... 
    Читать полностью

  • Редизайн сайта

    Редизайн сайта – это полное либо частичное обновление дизайна существующего сайта.... 
    Читать полностью

  • Консалтинг, услуги контент-менеджера

    Сопровождение любых интернет ресурсов;- Знание HTML и CSS- Поиск и обновление контента;-... 
    Читать полностью

  • Трафик из соцсетей

    Сравнительно дешевый способ по сравнению с поисковым и контекстным видами раскрутки... 
    Читать полностью

  • Поисковая оптимизация

    Поисковая оптимизация (англ. search engine optimization, SEO) — поднятие позиций сайта в результатах... 
    Читать полностью