(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
Stimulsoft Reports.Ultimate Single License Includes one year subscription, source code
IBM Domino Messaging Server Processor Value Unit (PVU) License + SW Subscription & Support 12 Months
Allround Automation PL/SQL Developer - 5 user license
Quest Software. TOAD for Oracle Edition
ABBYY Lingvo x6 Европейская Профессиональная версия, электронный ключ
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
СУБД Oracle "с нуля"
Один день системного администратора
Проект mic-hard - все об XP - новости, статьи, советы
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100