Можно ли объединить предварительное объявление и обычное объявление в один файл, а затем использовать его, как если бы они были разделены? - PullRequest
1 голос
/ 30 октября 2019

У меня есть эта сумасшедшая идея объединить заголовки предварительного объявления и файлы фактического объявления в один, выполнив некоторые трюки с макросами. Чтобы обеспечить некоторый контекст, ежедневная политика, которую я придерживаюсь для предварительных объявлений, выглядит следующим образом:

  • Каждый файл заголовка имеет свое дополнение "_fwd.hpp", которое содержит предварительные объявления всех объектов, объявляемых в будущем. заголовочный файл.
  • Я включаю заголовок прямого объявления, где достаточно прямого объявления фактической вещи
  • Я включаю заголовок обычного объявления в основном в файлах .cpp и только тогда, когда требуется фактическая информация о реализации(вещи, требующие размера реализации, наследования и т. д.)

Но наличие отдельного заголовка _fwd.hpp для каждого заголовка загрязняет проект, и его сложно поддерживать. Итак, я пришел к следующей идее объединить предварительную декларацию и фактическую декларацию в один файл, а затем включить их в соответствии с числом включений. Я придумываю эту первоначальную идею двойного включения заголовка;

foo.hpp

#if !defined(FOO_FWD_H)
#define FOO_FWD_H
    // Forward declarations goes here
    struct foo;
#else // has forward_declaration, include actual if not included yet

#if !defined(FOO_H)
    #define FOO_H
    struct foo{
       foo(){/*....*/}
    };
    // Normal declarations goes here
#endif // FOO_H

#endif // FOO_FWD_H

Если я включу "foo.hpp" один раз, я получу предварительное объявление foo, ноесли я включу его во второй раз в модуль перевода, я получу предварительное и фактическое объявление foo, что мне вполне подходит. (поскольку я все равно делаю то же самое, включите fwdecl в заголовок, актуально в cpp).

Таким образом, при использовании описанного выше варианта использования это выглядит так:

bar.hpp

#pragma once

#include "foo.hpp" // forward declaration

struct bar{
    bar(const foo& f);
};

бар. cpp

#include "bar.hpp" // bar + 1st inclusion of foo.hpp
#include "foo.hpp" // included 2nd time, actual implementation enabled

bar::bar(const foo& f){
    f.rab(); // requires actual implementation
}

Но, как вы можете себе представить, у этого подхода есть проблемы. Самая большая проблема заключается в том, что если foo.hpp, включенный в другой заголовок, tar.hpp и tar.hpp включен в bar.hpp, это приводит к тому, что фактическая реализация подвергается воздействию bar.hpp, что наносит ущерб цели. Кроме того, когда фактическая реализация foo.hpp требуется в bar.hpp, ее нужно включать дважды, что выглядит странно (у линтеров и инструментов, таких как iwyu, могут быть проблемы с этим).

Так что вопрос кипитвплоть до этого, можем ли мы на самом деле сделать эту работу таким образом, чтобы

  • , включая заголовки, которые с помощью этой идиомы не мешали состоянию включения других заголовков
  • , устраняя необходимостьдвойное включение при необходимости фактической реализации

Заранее спасибо.

ОБНОВЛЕНИЕ: (30/10/19 22:57 GMT + 2)

Улучшенная версия идиомы, основанная на ответе @IanAbbott:

Попробуйте вживую: repl.it

foo.hpp (наш единственный заголовок реализации fwdecl & decl)

    // (mgilor): we got ourselves quite a lot boilerplate code, 
    // maybe x-macro concept help us to move away boilerplate to
    // a separate file?

    #if defined(FOO_FWD_ONLY)
        #undef FOO_FWD_HPP // prevent accidental implementation inclusion on other headers
    #endif

    #if defined(FOO_FWD_ONLY) && !defined(FOO_FWD_HPP) 
        #define FOO_FWD_HPP 
        // forward declarations go here
        struct foo;
    #elif !defined(FOO_FWD_ONLY)

         // includer wants the full monty
        #if !defined(FOO_HPP)
            #define FOO_HPP
            // actual declarations go here

            struct foo{
                foo(){/*....*/}
                void do_things(){}
            };

        #endif // FOO_HPP

    #endif // FOO_FWD_HPP

    // undef the macro, so future includes does not get affected
    #undef FOO_FWD_ONLY

tar.hpp (fwdecl только потребитель foo)

    #pragma once

    #define FOO_FWD_ONLY
    #include "foo.hpp" // this header needs forward declaration

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif

    struct tar{
        tar(foo & f){ }
    };

bar. hpp (fwdecl только потребитель foo, также использует tar.hpp)

    #pragma once

    #include "tar.hpp" // tar consumed foo fwdecl-only
    #define FOO_FWD_ONLY
    #include "foo.hpp" // bar needs fwdecl-only

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif

    struct bar{
        bar(foo & f);
    };

bar.cpp (полный потребитель declbar & foo)

    #include "bar.hpp"
    #include "foo.hpp" // second inclusion, should enable full definition

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif

    bar::bar(foo& ref){
        ref.do_things();
    }

baz.hpp (без зависимостей)

    #pragma once

    struct baz{
        void do_baz();
    };

baz.cpp (полный decl-потребитель foo& baz)

    #include "baz.hpp"
    #include "foo.hpp"  // no prior include of foo, but since FOO_FWD_ONLY is not defined
                        // baz.cpp will get full declaration.

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif


    void baz::do_baz(){
        foo f;
        f.do_things(); // completely fine.
    }

main.cpp (приложение-потребитель)

    // consuming application
    #include "tar.hpp" 
    #include "bar.hpp" 
    #include "foo.hpp"  // already has previous foo fwdecl, so second inclusion will enable full declaration. 
                        // (also FOO_FWD_ONLY is not defined, so first inclusion would enable it too)
    #include "baz.hpp"
    int main(void){

        foo f;
        tar t(f);
        bar b(f);
        baz bz;
    }

Вывод при компиляции:

    tar.hpp:7:13: warning: tar.hpp has forward declaration of foo 
    bar.hpp:8:13: warning: bar.hpp has forward declaration of foo 
    bar.cpp:6:13: warning: bar.cpp has forward declaration of foo 
    bar.cpp:9:13: warning: bar.cpp has full declaration of foo 
    baz.cpp:9:13: warning: baz.cpp has full declaration of foo 
    tar.hpp:7:13: warning: tar.hpp has forward declaration of foo 
    bar.hpp:8:13: warning: bar.hpp has forward declaration of foo 

1 Ответ

1 голос
/ 30 октября 2019

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

Это выглядит примерно так:

foo.hpp

#if !defined(FOO_FWD_HPP)
#define FOO_FWD_HPP

// forward declarations go here

struct foo;

#endif // FOO_FWD_HPP

#if !defined(FOO_FWD_ONLY)
// includer wants the full monty
#if !defined(FOO_HPP)
#define FOO_HPP

// normal declarations go here

struct foo{
   foo(){/*....*/}
};

#endif // FOO_HPP
#endif // FOO_FWD_ONLY

#undef FOO_FWD_ONLY

bar.hpp

#pragma once

// only need forward declarations from foo.hpp
#define FOO_FWD_ONLY
#include "foo.hpp"

struct bar {
    bar(const foo& f);
};

bar.cpp

#include "bar.hpp"
#include "foo.hpp"

bar::bar(const foo& f){
    f.rab(); // requires actual implementation
}

ОсновнойПреимущество заключается в уменьшении объема компилируемого кода. Это не делает ничего, чтобы решить проблему непреднамеренного воздействия. Например, если «bar.hpp» включает в себя какой-то другой файл, который включает в себя «foo.hpp» без предварительного определения макроса FOO_FWD_ONLY, полные определения из «foo.hpp» будут представлены оставшейся части «bar.hpp».

...