Могу ли я предоставить неполный заголовок для класса C ++, чтобы скрыть детали реализации? - PullRequest
2 голосов
/ 27 сентября 2019

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

impl.cpp

#include <api.h>
#include <impl.h>
Class::Class() {
    init();
}
Class::init() {
    myData = SomeLibrary::Type(42);
}
Class::doSomething() {
    myData.doSomething();
}

impl.h

#include <somelibrary.h>
class Class {
public:
    Class();
    init();
    doSomething();
private:
    SomeLibary::Type myData;
}

api.h

class Class {
    Class();
    doSomething();
}

Проблема в том, что мне не разрешено переопределять заголовки для определения класса.Это не работает, когда я определяю Class() и doSomething() только в api.h.


Возможный вариант - определить api.h и вообще не использовать его в проекте, но установите его (и не устанавливайте impl.h).

Очевидным недостатком является то, что мне нужно убедиться, что общие методы в api.h и impl.h всегда имеют одинаковую сигнатуру, в противном случае программы, использующие библиотеку, получат ошибки компоновщика, которые я не могу предсказатьпри компиляции библиотеки.

Но сработает ли этот подход вообще, или у меня возникнут другие проблемы (например, неправильные указатели на участников класса или подобные проблемы), поскольку файл obj не соответствует заголовку?

Ответы [ 2 ]

5 голосов
/ 27 сентября 2019

Короткий ответ: «Нет!»

Причина: любые / все «клиентские» проекты, которые должны использовать ваш класс Class, должны иметь полное объявление этого класса для того, чтобы компилятор мог правильно определять такие вещи, как смещения для переменных-членов.

Использование private членов хорошо - клиентские программы не смогут их изменить- как и ваша текущая реализация, где в заголовке представлены только краткие описания функций-членов со всеми фактическими определениями в вашем (личном) исходном файле.

Возможный способ обойти это - объявить указатель на вложенный класс в Class, где этот вложенный класс просто объявлен в общем заголовке: class NestedClass и тогда вы можете делать то, что вам нравится, с помощью этого вложенного указателя класса в вашемреализация.Как правило, указатель на вложенный класс можно сделать членом private;кроме того, поскольку его определение не дано в общем заголовке, любая попытка «клиентского» проекта получить доступ к этому классу (кроме как в качестве указателя) будет ошибкой компилятора.

Вот возможная разбивка кода (возможно, еще не без ошибок, поскольку это быстрый набор):

// impl.h
struct MyInternal; // An 'opaque' structure - the definition is For Your Eyes Only
class Class {
public:
    Class();
    init();
    doSomething();
private:
    MyInternal* hidden; // CLient never needs to access this! Compiler error if attempted.
}

// impl.cpp
#include <api.h>
#include <impl.h>

struct MyInternal {
    SomeLibrary::Type myData;
};

Class::Class() {
    init();
}
Class::init() {
    hidden = new MyInternal; // MUCH BETTER TO USE unique_ptr, or some other STL.
    hidden->myData = SomeLibrary::Type(42);
}
Class::doSomething() {
    hidden->myData.doSomething();
}

ПРИМЕЧАНИЕ. Как я уже говорил в комментарии к коду, было бы лучше использовать код std::unique_ptr<MyInternal> hidden.Тем не менее, это потребует от вас явных определений в вашем Class для деструктора, оператора присваивания и других (оператор перемещения? Конструктор копирования?), Так как для этого потребуется доступ к полному определению MyInternal структура.

2 голосов
/ 27 сентября 2019

Здесь вам может помочь идиома частной реализации (PIMPL).Это может привести к 2 заголовочным файлам и 2 исходным файлам вместо 2 и 1. Приведите глупый пример, который я на самом деле не пытался скомпилировать:

api.h

#pragma once
#include <memory>
struct foo_impl;
struct foo {
    int do_something(int argument);
private:
    std::unique_ptr<foo_impl> impl;
}

api.c

#include "api.h"
#include "impl.h"
int foo::do_something(int a) { return impl->do_something(); }

impl.h

#pragma once
#include <iostream>
struct foo_impl {
    foo_impl();
    int do_something(int);
    int initialize_b();
private:  
    int b;
};

impl.c

#include <iostream>
foo_impl::foo_impl() : b(initialize_b()} {  }
int foo_impl::do_something(int a) { return a+b++; }
int foo_impl::initialize_b() { ... }

foo_impl может иметь любые методы, которые ему нужны, как fooзаголовок (API) - это все, что увидит пользователь.Все, что нужно компилятору для компиляции foo, - это знание, что в качестве элемента данных имеется указатель, поэтому он может правильно определить foo.

...