如何使用库中的C++ 11中的'extern'将定义与类模板的声明分开(dll,so,..)

Ide*_*ent 20 c++ dll templates c++11

我是开源库的开发人员.我们的一个类是模仿一堆不同的类型.目前,该定义位于头文件中,这会对编译时间产生负面影响,并且还会强制用户包含多于所需的标头.我的目标是:

  • 为了减少编译时间,我想使用C++ 11引入的显式实例化声明.

  • 类方法和成员的定义都是静态的,必须与实现文件中的声明分开.它们应该可以在库外部和内部使用,而无需用户进行显式实例化定义或类似的任何操作.

  • 这必须在支持C++ 11的所有常见编译器上运行跨平台(Visual Studio 2013 +,GCC等)

C++ 11为类模板提供了新功能,特别是" 显式实例化声明 ".据我所知,这可以在这种情况下使用.以前的问题在类似的上下文中处理了这个问题,例如如何使用extern模板分离模板类的定义/实例化而没有'extern'但是那些不处理库导出,如果客户端尝试使用共享库,它们的解决方案会导致链接器错误.

目前我已经设法以在Visual Studio 2015上编译,链接和运行的方式实现它,但我不确定我是否正确使用了关键字,尤其是在这种情况下的__declspec.这就是我得到的(简化):

// class.h
template<typename T>
class PropertyHelper;

template<typename T>
class PropertyHelper<const T>
{
public:
    typedef typename PropertyHelper<T>::return_type return_type;

    static inline return_type fromString(const String& str)
    {
        return PropertyHelper<T>::fromString(str);
    }

    static const int SomeValue;
};


template<>
class EXPORTDEF PropertyHelper<float>
{
public:
    typedef float return_type;

    static return_type fromString(const String& str);

    static const int SomeValue;
};

extern template EXPORTDEF class PropertyHelper<float>;
Run Code Online (Sandbox Code Playgroud)

最后一行是显式实例化声明.据我所知,这意味着客户不必每次都自己申报.EXPORTDEF是Windows上的__declspec(dllexport)或__declspec(dllimport)的定义.我不确定是否需要将其放在上面的行中,因为以下内容还编译,链接和运行:

extern template class PropertyHelper<float>;
Run Code Online (Sandbox Code Playgroud)

cpp文件如下所示:

const int PropertyHelper<float>::SomeValue(12);

PropertyHelper<float>::return_type
PropertyHelper<float>::fromString(const String& str)
{
    float val = 0.0f;

    if (str.empty())
        return val;

    //Some code here...

    return val;
}

template class PropertyHelper<float>;
Run Code Online (Sandbox Code Playgroud)

最后一行是显式实例化定义.

所以我的问题最重要的是如果我根据C++ 11标准在这里正确地做了一切,其次(如果第一个是真的)如果__declspec关键字在显式实例化声明的上下文中是多余的,或者我应该怎么做它,因为我没有在MSDN文档中找到适当的信息.

JVe*_*ene 11

标准(从C++ 0x的工作草案到2014年的工作草案),第14.7.2节描述了显式实例化,声明有两种形式的显式实例化,定义和声明.它说,"一个显式的实例化声明以extern关键字开头." 它进一步指定使用extern的声明不生成代码.

必须注意确保在模板类声明的命名空间内发出声明,或者特别引用限定名称中的命名空间,如下所示:

namespace N {
 template< class T > void f( T& ) {}
}

template void N::f<int>(int &);
Run Code Online (Sandbox Code Playgroud)

实例化模板函数,并生成它的代码(定义).鉴于:

extern template void N::f<int>(int &);
Run Code Online (Sandbox Code Playgroud)

将int类型的模板函数实例化为声明,但不生成代码.extern关键字通知编译器代码将在链接时从另一个源(可能是动态库)提供,但标准不讨论该平台特定的概念.

此外,可以有选择地实例化成员和成员函数,如下所示:

namespace N {
template<class T> class Y { void mf() { } };
}

template void N::Y<double>::mf();
Run Code Online (Sandbox Code Playgroud)

这仅为函数mf()生成代码,用于双精度.因此,可以声明实例化(使用extern),然后为模板类型的特定部分定义瞬时(无外部).可以选择在每个编译单元(内联)中为模板类的某些部分生成代码或成员,并强制将代码的其他部分生成到特定的编译单元或库中.

IBM的知识中心文章针对他们的XLC V 11.1编译器,支持草案C++ 0x,讨论了在构建库时使用extern关键字的策略.从他们的例子和标准文档草案开始的几年(从2008年开始就此问题一直保持一致),很明显extern对动态库的细节的适用性有限,但一般仅限于控制生成代码的位置.放置.仍然需要作者遵守与动态链接(和加载)相关的平台特定要求.这超出了extern关键字的目的.

Extern同样适用于静态库或动态库,但对库设计的限制很重要.

假设在头文件中呈现的模板类声明存在,如:

   namespace N
    {
     template< typename T >
     class Y
        { 
          private:

          int x;
          T v;

          public:

          void f1( T & );
          void f2( T &, int );
        };    
    }
Run Code Online (Sandbox Code Playgroud)

接下来,在CPP文件中:

namespace N
{
 template< typename T> void Y<T>::f1( T & ) { .... }
 template< typename T> void Y<T>::f2( T &, int ) { .... }
}
Run Code Online (Sandbox Code Playgroud)

现在考虑Y的潜在用途.库的消费者可能只需要为int,float和double实例化Y. 所有其他用途都没有价值.这是图书馆作者的设计点,而不是关于这个概念的一般概念.无论出于何种原因,作者只支持T的这三种类型.

为此,显式实例化声明可以包含在头文件中

extern template class N::Y< int >;
extern template class N::Y< float >;
extern template class N::Y< double >;
Run Code Online (Sandbox Code Playgroud)

由于这是由用户的各种编译单元处理的,因此通知编译器将为这三种类型生成代码,但是在用户构建时不会在每个编译单元中生成代码.实际上,如果作者没有包含定义模板类Y的函数f1和f2的CPP文件,则用户将无法使用该库.

假设,目前,静态库是关于模板类Y的预期产品(为了简化本讨论),作者使用CPP定义函数f1和f2以及显式实例化定义来编译静态库:

template class N::Y< int >;
template class N::Y< float >;
template class N::Y< double >;
Run Code Online (Sandbox Code Playgroud)

这将导致代表这三种类型为模板类Y生成代码,从而创建一个静态库.用户代码现在必须链接到此库,但不执行任何其他操作.他们的编译单元不会为模板类Y生成代码,而是从库中合并代码.

相同的概念适用于动态库,但关于函数声明,动态加载和动态链接的平台细节在C++到2014年工作草案的标准中找不到,关于C++ 0x,C++ 11或C++ 14,现在.显式模板实例化中的extern关键字仅限于创建声明,缺少创建定义(生成代码的位置).

这提出了关于这样的库的用户的问题,该库意图将Y用于无符号long,char或动态或静态库中未提供的某种其他类型.作者可以选择拒绝支持这一点,因为不分发代码生成源(模板类Y的f1和f2的函数定义).但是,如果作者确实希望支持这种用法,分发该源,则需要指示用户生成新库以替换现有库,或者为其他类型生成第二库.

对于任何一种情况,最好将CPP文件中的显式实例化定义分开,其中包括头部声明模板类Y,包括f1的函数定义的头部和模板类Y的f2(与实践相反)包括CPP文件,也可以工作).通过这种方式,用户可以创建一个CPP文件,其中包含模板类Y的头,然后是模板类Y的函数定义,然后发出新的显式实例化定义:

#include "ydeclaration.h" // the declaration of template class Y
#include "ydefinition.h"  // the definition of template class Y functions (like a CPP)

template class N::Y< unsigned long >;
template class N::Y< char >;
Run Code Online (Sandbox Code Playgroud)

对于静态库,几乎不需要其他任何东西,用户可以选择在他们的项目中构建额外的编译单元,从而避免了对静态库目标的需求.

但是,如果用户想要构建动态库,则需要注意关于特定平台上的动态库的平台特定代码.例如,特别是在Windows上,这可能意味着明确加载新的动态库.

考虑到创建动态库所涉及的复杂性,任何人都可以做到这一点.有时根本就没有别的选择.决定的关键是确定应该使用动态库的确切原因.在具有低于1 GB的RAM的计算机的古老时代中,其中一个理由是通过共享代码来节省内存,但对于任何特定的库,代码共享将导致节省RAM的概率是多少?对于像C运行时或Windows MFC DLL那样常见的东西,它可能很有可能.另一方面,提供高度针对性服务的库更可能仅由一个正在运行的程序使用.

一个非常好的目的是插入行为的概念.浏览器,IDE,照片编辑软件,CAD软件和其他应用程序受益于作为插件分发到现有产品的整个应用程序行业,这些产品作为动态库分发.

另一个理由是分发更新.虽然这是一个有吸引力的理论,但这种做法可能导致更多的问题而不是它的价值.

另一个常见的理由是"模块化".但到底是什么时候?分离编译单元已经减少了编译时间.动态库会影响链接时间而不是编译时间,但这是否值得额外的复杂性?

否则,提供动态库,特别是对于相当小的产品,并不值得麻烦.

可以编写整本书来编写适用于Windows和Linux的可移植动态库.

在Windows上,使用__declspec(dllexport/dllimport)的选择可以应用于整个类.然而,重要的是要意识到,无论使用何种编译器生成DLL,都只能与使用相同编译器或兼容编译器构建的目标一起使用.在MS VC谱系中,许多版本在此级别上彼此不兼容,因此使用一个版本的Visual Studio构建的DLL可能与其他版本不兼容,从而导致作者为每个可能的编译器生成DLL的负担/要支持的版本.

有关静态库的类似问题.客户端代码必须与构建DLL的CRT的相同版本和配置链接(CRT是否静态链接?).客户端代码还必须选择与构建库时相同的异常处理设置和RTTI设置.

当考虑到Linux或UNIX(或Android/iOS)的可移植性时,问题会放大.动态链接是一种特定于平台的概念,未在C++中处理.

静态库可能是最好的方法,对于那些__declspec(dllexport/dllimport)不应该使用.

对于动态库所说的一切,这是在Windows中实现它的许多方法之一(完全不适用于Linux/UNIX /等).

一种简单的(可能是天真但方便的)方法是将整个类视为从DLL导出(在客户端代码中导入).这比将每个函数声明为导出或导入稍有优势,因为这种方法包括类数据,同样重要的是,AUTOMATIC赋值/析构函数/构造函数代码C++可以为您的类构建.如果您没有仔细处理这些并手动导出,那么这可能非常重要.

在DLL生成中包含的标头中:

#define DLL_EXPORT // or something similar, to indicate the DLL is being built
Run Code Online (Sandbox Code Playgroud)

这将包含在标题顶部,声明您的库的模板类.声明DLL_EXPORT的标头仅用于配置为编译DLL库的项目中.所有客户端代码都将导入其他空版本.(无数其他方法存在).

因此,DLL_EXPORT是在为DLL构建时定义的,未在构建客户端代码时定义.

在库的模板类声明的标题中:

#ifdef _WIN32 // any Windows compliant compiler, might use _MSC_VER for VC specific code
#ifdef DLL_EXPORT
#define LIB_DECL __declspec(dllexport)
#else
#define LIB_DECL __declspec(dllimport)
#endif
Run Code Online (Sandbox Code Playgroud)

或者你喜欢用什么来代替LIB_DECL,作为声明可以从DLL导出的整个类的方法,在客户端代码中导入.

继续进行类声明:

namespace N
    {
     template< typename T >
     struct LIB_DECL Y
        { 
          int x;
          T v;
          std::vector< T > VecOfT;

          void f1( T & );
          void f2( T &, int );
        };    
    }
Run Code Online (Sandbox Code Playgroud)

对此的显式实例化声明将是:

extern template struct LIB_DECL N::Y< int >;
extern template struct LIB_DECL N::Y< float >;
extern template struct LIB_DECL N::Y< double >;

extern template class LIB_DECL std::vector< int >;
extern template class LIB_DECL std::vector< float >;
extern template class LIB_DECL std::vector< double >;
Run Code Online (Sandbox Code Playgroud)

请注意本例中Y类中使用的std :: vector.仔细考虑问题.如果您的DLL库使用std :: vector(或任何STL类,这只是一个示例),那么您在构建DLL时使用的实现必须与用户在构建客户端代码时选择的实现相匹配.向量的3个显式实例化匹配模板类Y的要求,并在DLL中实例化std :: vector,并且此声明可从DLL导出.

考虑一下,DLL代码如何编写USE std :: vector.什么会在DLL中生成代码?从经验来看很明显,std :: vector的源是内联的 - 它只是一个头文件.如果你的DLL确实实例化了vector的代码,客户端代码将如何访问它?客户端代码将"看到"std :: vector源并尝试自己内联生成该代码,其中客户端访问std :: vector函数.如果且仅当两者完全匹配时才会起作用.用于构建DLL的源与用于构建客户端的源之间的任何差异都会有所不同.如果客户端代码可以访问模板类T中的std :: vector,那么当使用std :: vector时,如果客户端使用不同的版本或实现(或具有不同的编译器设置),则会出现混乱.

您可以选择显式生成std :: vector,并通过将std :: vector声明为extern模板类来通知客户端代码使用该生成的代码,以便在客户端代码中导入(在DLL构建中导出).

现在,在构建DLL的CPP中 - 库的函数定义 - 您必须显式实例化定义:

template struct LIB_DECL N::Y< int >;
template struct LIB_DECL N::Y< float >;
template struct LIB_DECL N::Y< double >;

template class LIB_DECL std::vector< int >;
template class LIB_DECL std::vector< float >;
template class LIB_DECL std::vector< double >;
Run Code Online (Sandbox Code Playgroud)

在某些示例中,如MS KB 168958,他们建议将extern关键字定义为define,将此计划修改为:

#ifdef _WIN32 // any Windows compliant compiler, might use _MSC_VER for VC specific code
#ifdef DLL_EXPORT
#define LIB_DECL __declspec(dllexport)
#define EX_TEMPLATE
#else
#define LIB_DECL __declspec(dllimport)
#define EX_TEMPLATE extern
#endif
Run Code Online (Sandbox Code Playgroud)

因此,在DLL和客户端构建的头文件中,您可以简单地说明

EX_TEMPLATE template struct LIB_DECL N::Y< int >;
EX_TEMPLATE template struct LIB_DECL N::Y< float >;
EX_TEMPLATE template struct LIB_DECL N::Y< double >;

EX_TEMPLATE template class LIB_DECL std::vector< int >;
EX_TEMPLATE template class LIB_DECL std::vector< float >;
EX_TEMPLATE template class LIB_DECL std::vector< double >;
Run Code Online (Sandbox Code Playgroud)

虽然这有一次发布这些行的优点,但在标题中,我个人更喜欢在标题中明确地使用extern关键字,因此我毫无疑问地知道可以发生的唯一地方代码生成是在CPP中DLL构建(第二次出现,没有extern关键字).通过这种方式,标头中的extern是前向声明,它与CPP中的显式实例化定义不冲突,并且避免在客户端代码中使用时模糊extern关键字.这也许是我自己特有的偏好.

你可能会想,"其他客户端代码和std :: vector怎么样".嗯,重要的是要考虑.您的头文件包含std :: vector,但请记住,您的DLL是使用您在编译时可用的代码构建的.您的客户端将拥有自己的标头,它在VC的相同版本中,应该是相同的.但是,应该不是一个好的计划.可能会有所不同.他们可能只是假设VC 2015是相同的并且继续前进.任何差异,无论是对象布局,实际代码......都可以使用非常奇怪的效果来破坏正在运行的应用程序.如果您导出您的版本,他们建议客户端在其所有编译单元中包含显式实例化声明,因此所有内容都使用您的std :: vector版本...但是,有一个严重的问题.

如果其他一些库也使用不同版本的std :: vector会怎么做呢?

它在这些环境中使用STL有点讨厌,所以有一个相当不错的设计选择可以消除这种情况.不要暴露任何STL的使用.

如果您将所有STL私有使用到库中,并且从不将STL容器暴露给客户端代码,那么您可能已经清楚了.如果在设计中选择它,则无需在库中显式实例化std :: vector(或任何STL).

我包含了这个例子,以便讨论它,MS如何记录它(KB 168958),以及为什么你可能不想这样做.但是,相反的情况也出现了.

在原始查询中,将使用std :: string(或其中一个替代).想一想:在DLL中,如何使用std :: string实例化?如果构建DLL时可用的std :: string代码与构建它们的客户端使用的代码之间有什么区别怎么办?毕竟,客户可以选择使用除MS提供的其他STL.当然,你可以规定他们不这样做,但是......也许你可以在你的DLL中明确地将std :: string作为extern实例化.这样,您就没有在DLL中构建STL代码,现在通知编译器应该找到客户端而不是DLL内部构建的代码.我建议用于研究和思考.

你面临的阴险问题是:在你的测试中,这一切都将在你的机器上运行,因为你正在使用一个编译器.它会完美无瑕,但由于代码差异或设置差异,微妙到足以逃避警告,在客户端构建中可能会失败.

所以,让我们假设您同意并跳过实例化std :: vector的示例中的最后三行...它完成了吗?

这取决于您的IDE设置,我将留给您.问题集中在使用__declspec(dllxxxxx)及其用法,有几种方法可以实现它的使用,我专注于一个.无论您是否必须显式加载库,依赖于自动动态链接功能,请考虑DLL_PATH ...这些是您知道或超出问题实际范围的一般DLL构建主题.

  • 这是一个非常详细的回复,谢谢。阅读有关动态库与静态库的长篇讨论也很有趣。目前我们的重点是动态库,而静态链接只支持中途,没有人维护/测试它。我将来改变这个没有问题。但是我记得也有关于静态链接的问题/担忧,但我不记得细节,并且不是关于更喜欢哪种类型的链接的决策过程的一部分。我们也有人混合编译器然后抱怨;) (2认同)