(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

C++ MythBusters. Миф о подставляемых функциях (исходники)

Источник: habrahabr
GooRoo

Сегодня я хочу начать цикл статей, призванных помочь именно новичкам, относительно недавно начавшим изучать этот язык, либо же тем, кто (упаси Боже) читает мало книг, а пытается познавать всё исключительно на практике.

Также я надеюсь привлечь? как можно больше авторов к написанию подобных статей, потому как моего опыта здесь будет явно недостаточно.

Лирическое отступление

Несколько слов о названии, призванном объединить статьи подобного рода. Оно, естественно, появилось не случайно, однако и не совсем соответствует сути.

Мои статьи будут рассчитаны на тех, кто уже более-менее знаком с языком C++, но у кого мало опыта в написании программ на нем. Я не буду писать мануалы или "вводные" пособия в духе "ведер", "чашек", "унций" и т.п. Вместо этого я попытаюсь освещать некоторые "узкие" и не всегда и не совсем очевидные места языка C++. Что имеется в виду? Я не раз сталкивался, как на собственном примере, так и при общении с другими программистами, со случаями, когда человек был уверен в своей правоте касательно какой-нибудь возможности языка C++ (иногда довольно длительное время), а в последствие оказывалось, что он глубоко и бесповоротно заблуждался по одному Богу известным причинам.

А причины на самом деле не такие уж и сверхъестественные. Зачастую свою роль играет человеческий фактор. К примеру, прочитав какую-либо книгу для начинающих, в которых, как известно, многие нюансы не объясняются, а иногда даже не упоминаются, дабы упростить восприятие основ языка, читатель додумывает недостающие вещи самостоятельно из соображений a la "по-моему, это логично". Отсюда возникают крупицы недопонимания, иногда приводящие к довольно серьезным ошибкам, ну, а в большинстве случаев просто мешающие успешному прохождению различного рода олимпиад по С++ :)

Итак, миф первый

Как известно, в языке C++ есть возможность объявления подставляемых функций. Это реализуется за счет использования ключевого слова inline. В месте вызова таких функций компилятор сгенерирует не команду call (с предварительным занесением параметров в стек), а просто скопирует тело функции на место вызова с подстановкой соответствующих параметров "по месту" (в случае методов класса компилятор также подставит необходимый адрес this там, где он используется). Естественно inline - это всего лишь рекомендация компилятору, а не приказ, однако в случае, если функция не слишком сложная (достаточно субъективное понятие) и в коде не производятся операции типа взятия адреса функции etc., то скорее всего компилятор поступит именно так, как того ожидает программист.

Подставляемая функция объявляется достаточно просто:

inline void foo(int & _i)
{
  _i++;
}

Но речь сейчас не об этом. Мы рассмотрим использование подставляемых методов класса. И начнем с небольшого примера, по вине которого и может возникать данный миф.

Все вы знаете, что определения методов класса можно писать как снаружи класса, так и внутри, и подставляемые функции здесь не исключение. Притом функции, определенные прямо внутри класса, автоматически становятся подставляемыми и ключевое слово inline в таком случае излишне. Рассмотрим пример (использую struct вместо class только для того чтобы не писать public):

// InlineTest.cpp

#include
#include

struct A
{
  inline void foo() { std::cout << "A::foo()" << std::endl; }
};

struct B
{
  inline void foo();
};

void B::foo()
{
  std::cout << "B::foo()" << std::endl;
}

int main()
{
  A a; B b;
  a.foo();
  b.foo();
  return EXIT_SUCCESS;
}

В данном примере все отлично, и на экране мы видим заветные строки:

A::foo()
B::foo()

Причем компилятор действительно подставил тела методов в места их вызовов.

Наконец-то мы подобрались к сути сегодняшней статьи. Проблемы начинаются в тот момент, когда мы (соблюдая "хороший стиль программирования") разделяем класс на cpp- и h-файлы:

// A.h

#ifndef _A_H_
#define _A_H_

class A
{
public:
  inline void foo();
};

#endif // _A_H_

// A.cpp

#include "A.h"

#include

void A::foo()
{
  std::cout << "A::foo()" << std::endl;
}

// main.cpp

#include
#include
#include "A.h"

int main()
{
  A a;
  a.foo();

  return EXIT_SUCCESS;
}

На стадии линковки получаем ошибку вроде такой (зависит от компилятора - у меня MSVC):

main.obj: error LNK2001: unresolved external symbol "public: void __thiscall A::foo (void)" (? foo@A@@QAEXXZ)

Почему?! Всё достаточно просто: определение подставляемого метода и её вызов находятся в разных единицах трансляции! Не совсем уверен, как именно это устроено внутренне, но я вижу эту проблему так:

если бы это был обычный метод, то в единице трансляции main.obj компилятор бы поставил нечто вроде call XXXXX, а позже уже компоновщик заменил бы XXXXX на конкретный адрес метода A::foo() из единицы трансляции A.obj (конечно же, я всё упростил, но суть не меняется).

В нашем же случае мы имеем дело с inline-методом, то есть вместо вызова компилятор должен подставить непосредственно текст метода. Так как определение находится в другой единице трансляции, компилятор оставляет эту ситуацию на попечение компоновщика. Здесь есть два момента: во-первых, "сколько места должен оставить компилятор для подстановки тела метода?", а во-вторых, в единице трансляции A.obj метод A::foo() нигде не используется, причем метод объявлен как inline (а значит там, где нужно было, компилятор должен был скопировать тело метода), поэтому отдельная скомпилированная версия этого метода в итоговый объектный файл не попадает вообще.

В подтверждение пункта 2 приведу немного дополненный пример:

// A.h

#ifndef _A_H_
#define _A_H_

class A
{
public:
  inline void foo();
  void bar();
};

#endif // _A_H_

// A.cpp

#include "A.h"

#include

void A::foo()
{
  std::cout << "A::foo()" << std::endl;
}

void A::bar()
{
  std::cout << "A::bar()" << std::endl;
  foo();
}

// main.cpp

#include
#include
#include "A.h"

int main()
{
  A a;
  a.foo();

  return EXIT_SUCCESS;
}

Теперь всё работает, как и должно, благодаря тому, что inline-метод A::foo() вызывается в неподставляемом методе A::bar(). Если взглянуть на ассемблерный код итогового бинарника, можно увидеть, что, как и раньше, отдельной скомпилированной версии метода foo() нет (то есть у метода нет своего адреса), а тело метода скопировано непосредственно в места вызова.

Как выйти из этой ситуации? Очень просто: подставляемые методы нужно определять непосредственно в header-файле (не обязательно внутри объявления класса). При этом ошибки повторного определения не возникает, так как компилятор говорит компоновщику игнорировать ошибки ODR (One Definition Rule), а компоновщик в свою очередь оставляет только одно определение в результирующем бинарном файле.

Заключение

Надеюсь, хоть кому-то моя первая статья станет полезной и чуточку поможет достигнуть полного осознания такого странного и местами противоречивого, но, безусловно, интересного языка программирования, как C++. Успехов:)

UPD. В процессе общения с gribozavr была выявлена некоторая неточность касательно ODR в моей статье. Выделил курсивом.

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 11.02.2009 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
DevExpress / ASP.NET Subscription
ESET NOD32 Антивирус на 1 год для 3ПК или продление на 20 месяцев
Business Studio 4.2 Enterprise. Конкурентная лицензия + Business Studio Portal 4.2. Пользовательская именная лицензия.
SmartBear Collaborator - Named User License (Includes 1 Year Maintenance)
erwin Data Modeler Workgroup Edition r9.7 - Product plus 1 Year Enterprise Maintenance Commercial
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
СУБД Oracle "с нуля"
Программирование на Visual С++
Новые программы для Windows
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100