【问题标题】:C++ DLLs: how to create the corresponding include headers?C++ DLL:如何创建相应的包含头文件?
【发布时间】:2021-12-01 05:25:50
【问题描述】:

问题

我有一个 C++ 项目并从中创建了一个库 A。如果我现在将另一个项目 B 与这个库 A 链接,我当然还必须为 A 的头文件提供包含路径,所以我只使用 A 的源文件夹。但是 A 的标头包含未导出的符号。我觉得这不是正确的方法,但不知道更好。让我觉得这是不正确的一个具体事情是我的 IDE 建议了未导出的符号。

我猜解决方案是在源文件夹之外创建一个包含文件夹,其中包含相同的标头,但仅包含导出的符号。所以在构建时,每个带有PROJECTAPI 的符号都应该自动复制到包含文件夹中的相应标题中。但是如果我用谷歌搜索,我找不到这样的功能,例如cmake。

那么这里推荐的方式是什么?是否有创建此类包含文件夹的功能?

示例

项目B的example.cpp

#include <A/main.hpp>

int main() {
    ex::World w("Earth");
    w.say_hello();
    //IDE wouldn't see this as error: w.private();
}

项目A的main.cpp

#include <iostream>
#include "main.hpp"

namespace ex {
void World::say_hello() {
    std::cout << "Hello, World from " << m_name << std::endl;
}

World::World(std::string name)
    : m_name(name)
{}

void World::hidden() {
    std::cout << "Not exported" << std::endl;
}
}

项目A的main.hpp

#include <string>

#ifndef PROJECTAPI
#  ifdef example_EXPORTS
#    define PROJECTAPI __declspec(dllexport)
#  else
#    define PROJECTAPI __declspec(dllimport)
#  endif
#endif

namespace ex {
class World {
private:
    std::string m_name;
public:
    void PROJECTAPI say_hello();
    PROJECTAPI World(std::string name);
    void hidden();
};
}

编辑:private 不是一个好的方法名称

【问题讨论】:

  • 这些是特殊的.h 用于导出
  • 这不仅仅是关于符号,因为您的示例具有一个类。您不能创建包含与用于编译库不同的类定义的标头。
  • @Frank 我可以在项目 A 中创建一个与 main.hpp 相同但不包含 void hidden() 的 interface.hpp 并导入接口而不是主标头。
  • @VincentHilla 在 C++ 中,标头不仅仅是要链接的符号列表,该语言要求类定义的所有副本在所有翻译单元中都相同.它可能“有效”,但它是未定义的行为。所以它的工作只是你不应该依赖的幸运巧合。

标签: c++ visual-c++ cmake build


【解决方案1】:

您正在寻找 PIMPL 成语。 “PIMPL”是“指向实现的指针”的缩写。这个想法是,以指针间接为代价,您将实现数据和私有方法隐藏在内部类中,其定义对于 API 使用者不透明

如果您需要提供 ABI 稳定性,这种方法特别有效。

Herb Sutter 对此有很好的 GOTW:https://herbsutter.com/gotw/_100/


这是一个与您的代码非常接近的完整示例:

$ tree
.
├── CMakeLists.txt
├── include
│   └── world.h
├── main.cpp
└── src
    ├── world.cpp
    └── world_priv.h

./include/world.h(公共标头)中

#ifndef WORLD_H
#define WORLD_H

#include <memory>
#include <string>

#include "world_export.h"

namespace ex {

class World {
public:
  WORLD_EXPORT World(std::string name);
  WORLD_EXPORT ~World() /* = default */;

  void WORLD_EXPORT say_hello();

private:
  class Impl;
  std::unique_ptr<Impl> pImpl;
};

} // namespace ex

#endif

./src/world_priv.h:

#ifndef WORLD_PRIV_H
#define WORLD_PRIV_H

#include "world.h"

namespace ex {

class World::Impl {
public:
  Impl(std::string name) : name(std::move(name)) {}

  void say_hello();
  void hidden();

private:
  std::string name;
};

} // namespace ex

#endif

./src/world.cpp:

#include <iostream>

#include "world_priv.h"

namespace ex {

World::World(std::string name)
    : pImpl(std::make_unique<Impl>(std::move(name))) {}
World::~World() = default;

void World::say_hello() { pImpl->say_hello(); }

void World::Impl::say_hello() {
  std::cout << "Hello, World from " << name << "\n";
}

void World::Impl::hidden() { std::cout << "Not exported" << std::endl; }

} // namespace ex

main.cpp:

#include <world.h>

int main() {
  ex::World w("Earth");
  w.say_hello();
}

最后,这是构建:

cmake_minimum_required(VERSION 3.21)
project(pimpl_example)

option(BUILD_SHARED_LIBS "Build world as shared rather than static" ON)

# Library

include(GenerateExportHeader)

set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)

add_library(world src/world.cpp src/world_priv.h include/world.h)
add_library(world::world ALIAS world)

target_include_directories(
  world PRIVATE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>"
        PUBLIC  "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
)

generate_export_header(world EXPORT_FILE_NAME include/world_export.h)
target_compile_definitions(
    world PUBLIC "$<$<NOT:$<BOOL:${BUILD_SHARED_LIBS}>>:WORLD_STATIC_DEFINE>")
target_include_directories(
    world PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>")

# Application

add_executable(app main.cpp)
target_link_libraries(app PRIVATE world::world)

这不包括安装规则或任何东西,但它已准备好编写。


构建它:

$ cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
...
$ cmake --build build
$ ./build/app
Hello, World from Earth
$ $ nm ./build/libworld.so | c++filt | grep ' T ' | uniq
0000000000001460 T ex::World::say_hello()
00000000000012e0 T ex::World::World(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
00000000000013c0 T ex::World::~World()

可以看到库只导出了ex::World的API。所有私人细节都隐藏在代码和库本身中。

在 Windows 上:

>dumpbin /EXPORTS build\world.dll
Microsoft (R) COFF/PE Dumper Version 14.28.29915.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file build\world.dll

File Type: DLL

  Section contains the following exports for world.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           3 number of functions
           3 number of names

    ordinal hint RVA      name

          1    0 00001390 ??0World@ex@@QEAA@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z
          2    1 00001550 ??1World@ex@@QEAA@XZ
          3    2 000015D0 ?say_hello@World@ex@@QEAAXXZ

  Summary

        1000 .data
        1000 .pdata
        2000 .rdata
        1000 .reloc
        1000 .rsrc
        2000 .text

【讨论】:

    【解决方案2】:

    通常,解决此问题的一种简单方法是提前创建单独的公共和私有标头,并且只向用户公开公共标头。

    这是一个简单的项目结构,可以实现这一点:

    - lib_a
     - include
       - main.hpp
     - src
       - main_private.hpp
       - main.cpp
    

    现在,显然,这不适用于您发布的代码,因为您要分离的声明属于同一个类。但这只是一个症状,不幸的是,您尝试做的事情是不允许的。

    来自标准basic.def.odr

    可以有多个定义 (13.1) 类类型([class]),

    [...]

    在程序中,前提是每个定义出现在不同的翻译单元中,并且定义满足以下要求。

    [...]

    每个这样的定义都应由相同的标记序列组成 [...]

    换句话说,如果您将一个类放在公共标头中,它必须与编译库时使用的相同。

    尽管方便​​,但不允许将“半类”放在公共标头中。

    【讨论】:

      猜你喜欢
      • 2012-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-03
      • 2021-08-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多