Скрытие кода детального пространства имен - элегантно

Предположим, я пишу библиотеку только для заголовков или в основном для заголовков и имею следующий код:

using my_type = int;

namespace detail {
    inline void foo() { my_type x; do_foo_stuff(x); }
}

inline void bar() { do_bar_stuff(); detail::foo(); }
inline void baz(my_type y) { do_baz_stuff(y); detail::foo(); }

Я хочу разместить foo()в другом файле. Мотивация заключается в том, что у меня есть много таких подробных и неточных функций, и я хочу, чтобы заголовок с моим общедоступным API не был загроможден тем, что появляется внутриdetail, и не предназначен для прямого использования.

Вопрос в том, какой идиоматический способ сделать это?

  • Я не могу просто включить файл с detail::код в конце моего общедоступного заголовка - поскольку объявления должны быть сделаны к моменту их использования.

  • Я не могу просто включить файл с detail::код в начале моего общедоступного заголовка - поскольку они полагаются на некоторые общедоступные определения, например, типы и константы. Предположим, что они не полагаются ни на какие функции.

Так что это не может быть ни один из этих двух вариантов.

3 ответа

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

Однако это неверно:

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

Разделение заголовков и исходных файлов не выполняется на уровне общедоступного интерфейса / реализации. Это разделение кода - всего лишь артефакт того, как разработан C++ с его наследием. C++ не требует многопроходного компилятора, поэтому перед использованием требуется объявление и одно определение. Заголовки - это решение. Это не спецификации API.

Таким образом, неудачный вывод заключается в том, что C++ не имеет разделения API и реализации, и попытки использовать для этого заголовки будут безуспешными.

Теперь, чтобы ответить на ваш вопрос: идиоматический способ, который я знаю, действительно использовать details или implпространство имен. Понятно, что пространства имен, названные таким образом, содержат детали реализации библиотеки и не должны использоваться в пользовательском коде. Я лично не стал бы менять ваш первоначальный дизайн.


В C++20 наконец-то появились модули, которые afaik решают эту проблему. И теперь у нас есть четкое разделение внутренних символов, которые не будут видны в пользовательском и публичном API.

Если вам не требуется, чтобы реализация «public api» находилась в основном заголовке, и вас устраивают только сигнатуры функций, вы можете разделить объявления на основной заголовок, а определения на подробный заголовок следующим образом:

      //header.hpp
using my_type = int;

inline my_type bar();
inline my_type baz(my_type y);

#include "detail.hpp"
      // detail.hpp
#include <cstdlib>
namespace detail {
    inline my_type foo(my_type i) { return std::rand() + i; }
}

inline my_type bar() { return detail::foo(3); }
inline my_type baz(my_type y) { return detail::foo(y); }
      // main.cpp
#include "header.hpp"
#include <iostream>

int main() {
    std::cout << bar() << " "<< baz(4) << std::endl;
    return 0;
}

Скомпилировано с помощью gcc 11.3.

      $ g++ -std=gnu++14 main.cpp -Wall -Werror -Wpedantic && ./a.out
1804289386 846930890

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

#ifndef MY_DETAIL_HPP_TWICE
#ifndef MY_DETAIL_HPP_ONCE
#define MY_DETAIL_HPP_ONCE
#else
#define DEFINITIONS_VISIBLE
#define MY_DETAIL_HPP_TWICE
#endif // MY_DETAIL_HPP_ONCE

namespace detail {

inline void foo()
#ifndef DEFINITIONS_VISIBLE
;
#else
{ my_type x; do_foo_stuff(x); }
#endif

}
#endif // MY_DETAIL_HPP_TWICE

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

Другие вопросы по тегам