2

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

С точки зрения ООП, разве это правильно?

/* МоиРассуждения/b/

Можно бы было сделать "публичный" .h файл, в котором только публичные объявления, и "приватный", в котором бы были и публичные и приватные объявления, который будет инклюдить только файл определения реализации .cpp.

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

Какая разница компоновщику с private: сокрытых данных при связывании? Только если это файл опеределения реализации .cpp.

*/

Знаю что не прав, поправьте меня пожалуйста.

Пользуясь случаем, ради интереса, как это правило (odr) реализовано в true-ооп java?

(разницу между include и import понимаю)

uskabuska
  • 679

2 Answers2

3

Да, вы понимаете правильно: сокрытие данных в данном случае условное, и не защищает от повторной компиляции при изменении приватных данных. Это и правда не очень хорошо с точки зрения инкапсуляции, но с этой технической проблемой не так просто бороться в рамках языка C++.

К сожалению, вы не можете так просто разделить данные между несколькими файлами, потому что для компиляции необходимо знать memory layout класса, который включает в себя и приватные данные.

Общепринятый подход для решения этой проблемы — идиома pimpl: вы делите класс на публичную часть и приватную имплементацию, публичная часть содержит только публичные данные, а приватная часть содержит реальную имплементацию и приватные данные:

// dog.h
class Dog
{
    class DogImpl;
    DogImpl* p_impl;
public:
    Dog(std::string name);
    void bark();
    ~Dog();
};

// dog.cpp
#include "dog.h"
using namespace std;

class DogImpl
{
    string name;
    string barkRepresentation;
public:
    DogImpl(string name) : name(name), barkRepresentation(computeBark()) { }
    void bark() { cout << barkRepresentation; }
    static string computeBark() { return "Wow wow wow!"; }
};

// proxy functions
Dog::Dog(string name) : p_impl(new DogImpl(name)) { }
Dog::bark() { p_impl->bark(); }
Dog::~Dog() { delete p_impl; }

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

Вот обсуждение по теме PImpl на английском.

Обратите внимание, что часто проблему считают не настолько серьёзной, чтобы вручную бороться с недостатками языка, и обходятся традиционным способом: видимой в заголовочном файле приватной частью.


Правило ODR нужно по существу лишь из-за модели компиляции C++, в которой куски текста (header-файлы) включаются несколько раз в разных местах. Для языков с более современной моделью компиляции всё гораздо проще: никаких заголовочных файлов нет, так что правило сокращается до «класс с данным именем должен быть определён только один раз».

VladD
  • 206,799
  • А методы DogImpl должны быть inline или это ради сокращения примера? На счет java, знаю что выбор перегруженного метода выполняется на стадии компиляции, и если я изменю иерархию наследования в каком нибудь классе и скомпилирую .class, а исполняемый файл не перекомпилирую - на перегрузку это может повлиять. Какие правила там, не знаете? Никак не могу найти, даже в книге Шилда. PS: Я понимаю что там нет заголовочных файлов и понимаю разницу между инклюдами и импортами. – uskabuska Jul 07 '15 at 12:10
  • 1
    @uskabuska: Методы DogImpl вы можете определять как угодно. Я бы оставил inline, но если вам больше нравится вне класса, то можно и так. В любом случае имплементацию DogImpl никто не видит, кроме вас :) – VladD Jul 07 '15 at 12:12
  • @uskabuska: Думаю, что язык не описывает последовательность таких ужасных преступлений, как намеренная частичная перекомпиляция проекта. Это всё равно что спросить «если я выпущу на футбольное поле две команды, и одной из них скажу, что правила игры поменялись, а другой нет, будет ли всё хорошо?» Просто не делайте так, играйте по правилам. – VladD Jul 07 '15 at 12:15
  • @uskabuska: И я что-то сомневаюсь, что Шилдт — хороший источник. Вот здесь есть прекрасный список книг. – VladD Jul 07 '15 at 12:17
  • Огромное спасибо за информацию! –  Jul 07 '15 at 12:43
  • @uskabuska: Уточню: вы ищете, что будет, если вы попытаетесь обмануть язык, подсунув перекомпилированный модуль, но не обновив зависимые модули? – VladD Jul 07 '15 at 14:36
  • @uskabuska: Правило очень простое: изменился зависимый проект/файл — требуется перекомпиляция. – VladD Jul 07 '15 at 15:03
  • @uskabuska: Вы пытаетесь думать в каких-то очень низкоуровневых терминах. Java уже очень давно не интерпретатор, а компилятор. Если Main использует DoIt, а DoIt поменялся — Main надо перекомпилировать. – VladD Jul 07 '15 at 15:08
  • @VladD: Там компилируется все в байткод, а исполняется интерпретатором. Нутк, а если приватное поле Dolt изменилось, зачем в java, при такой модели, перекомпиляция? – uskabuska Jul 07 '15 at 15:09
  • 1
    @uskabuska: Это уже давно не так. Байткод уже давно компилируется JIT-компилятором в нативный код. – VladD Jul 07 '15 at 15:11
  • @uskabuska: Окей, понял теперь, о чём вы. Да, по идее если public/protected-часть класса не поменялась, то можно не перекомпилировать. – VladD Jul 07 '15 at 15:13
  • 1
    @VladD: Я вот только хочу найти подробные правила, в подтверждение. Или придется к каждому случаю применять логический вывод? А вдруг я ошибусь ;D – uskabuska Jul 07 '15 at 15:14
  • @uskabuska: Это стоит задать отдельным вопросом, только про Java. (Тут по идее ничего общего с ODR.) – VladD Jul 07 '15 at 15:16
  • @VladD: Хорошо, я создам отдельную тему. Спасибо Вам за ответ, на счет С++, вы меня уже много раз выручаете :) – uskabuska Jul 07 '15 at 15:27
  • @uskabuska: Пожалуйста! Всегда рад хорошему вопросу. – VladD Jul 07 '15 at 20:46
0

Ценой предлагаемого вами способа сокрытия реализации является то, что пользователь вашего класса не сможет самостоятельно объявлять объекты вашего класса, расположенные целиком в локальной или статической памяти. Раз компилятору не видно полное определение класса, то ему не известен и полный размер этого класса и, соответственно, создавать объекты такого класса в коде пользователя компилятор не может. Поэтому создавать объекты придется где-то в недрах кода, знакомого с полным устройством класса, и возвращать их наверх пользователю по указателю. Это означет, что обязательно придется использовать какую-то форму динамической памяти для хранения объекта или хотя бы "скрытых" его частей.

Например, в самом экстремальном варианте, содержимое класса не видно пользователю вообще

class CFileHandle;

В такой ситуации пользователь не может определять объекты такого класса самостоятельно. Он может только вызывать какую-то функцию данной библиотеки, которая будет внутри динамически создавать объекты данного класса (либо через new, либо еще как) и возвращать пользователю указатель на них

CFileHandle *create_file(/* whatever */);

Разнообразные "промежуточные" варианты сокрытия реализации, как популярная PIMPL idiom, описанная в ответе @VladD, точно таким же образом вынуждены пользоваться динамическим выделением памяти для сокрытой части объекта.

Требование безусловного использования динамической памяти испокон веков считалось неприемлемым языках С и С++, ориентированных на эффективность генерируемого кода (в языке С эта же проблема предстает в точно такой же форме). Также факт сокрытия реализации части класса не позволяет обрабатывать соответствующие скрытые части объектов при помощи inline-функций.

Поэтому сам язык С++ на его базовом уровне настроен именно на определение всего класса, со всеми потрохами, в заголовочном файле, тем самым давая пользователю возможность самостоятельно определять объекты такого класса и работать с ними максимально эффективно (без ненужных накладных расходов). А уж если вас не устраивает такая полная видимость определения класса и вы готовы немножко пожертвовать производительностью, то вам предлагается пользоваться рукописными идиомами типа PIMPL idiom.

  • Вопрос, насколько я понимаю, в жёсткой необходимости повторной компиляции всех зависимых файлов при изменении приватной части класса. Будь модель компиляции C++ посовременнее, достаточно было бы лишь компилятору видеть «потроха» (ценой простановки sizeof во время компоновки). – VladD Jul 07 '15 at 20:56
  • 1
    @VladD: Да, но это противоречило бы принципу раздельной компиляции единиц трансляции, на котором построен и С и С++. Влияние принципа раздельной компиляции по прежнему очень сильно в дизайне языка. Понятно, что в наше время от этого принципа можно было бы уже и отказаться, но это, наверное, будет уже совсем другой язык. – AnT stands with Russia Jul 07 '15 at 21:01