显式实例化 - 何时使用?

时间:2010-02-28 13:17:46

标签: c++ templates

经过几周的休息,我正在尝试使用David Vandevoorde和Nicolai M. Josuttis所着的模板 - 完整指南来扩展和扩展我的模板知识,以及我正在尝试的内容此时要理解的是模板的明确实例化。

我实际上并没有这样的机制问题,但我无法想象我想要或想要使用此功能的情况。如果有人能向我解释,我将不止感激。

4 个答案:

答案 0 :(得分:63)

如果您定义的模板类只想用于几种显式类型。

将模板声明放在头文件中,就像普通类一样。

将模板定义放在源文件中,就像普通类一样。

然后,在源文件的末尾,显式地仅实例化您想要的版本。

愚蠢的例子:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

来源:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

主要

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}

答案 1 :(得分:45)

直接从https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation复制:

  

您可以使用显式实例化来创建模板化类或函数的实例化,而无需在代码中实际使用它。因为在创建使用模板进行分发的库(.lib)文件时非常有用,所以未将实例化的模板定义放入对象(.obj)文件中。

(例如,libstdc ++包含std::basic_string<char,char_traits<char>,allocator<char> >的显式实例化(std::string)所以每次使用std::string的函数时,相同的函数代码都不需要复制到对象。编译器只需要将它们引用(链接)到libstdc ++。)

答案 2 :(得分:10)

显式实例化可以减少编译时间和对象大小

这些是它可以提供的主要收益。它们来自以下部分中详细描述的以下两种效果:

  • 从标题中删除定义,以防止构建工具重建包含程序
  • 对象重新定义

从标题中删除定义

显式实例化使您可以将定义保留在.cpp文件中。

当定义位于标头上并对其进行修改时,智能的构建系统将重新编译所有包含程序,这些包含程序可能包含数十个文件,从而使编译速度令人难以忍受。

在.cpp文件中放入定义确实有一个缺点,即外部库无法使用其自己的新类重用模板,但是下面的“从包含的标头中删除定义,但也将模板暴露给外部API”显示了一种解决方法。

请参见下面的具体示例。

重新定义对象:了解问题

如果仅在头文件上完全定义模板,则包含该头的每个编译单元最终都会针对所使用的每种不同的模板参数来编译其自己的模板的隐式副本。

这意味着大量无用的磁盘使用和编译时间。

这是一个具体示例,其中main.cppnotmain.cpp都隐含定义MyTemplate<int>,这是因为它们在这些文件中的用法。

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHub upstream

使用nm编译和查看符号:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

输出:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

man nm中,我们看到W表示弱符号,GCC选择它是因为它是模板函数。符号弱表示MyTemplate<int>的隐式生成的编译代码已在两个文件上编译。

它在链接时不会因多个定义而崩溃的原因是the linker接受多个弱定义,只选择其中一个放入最终可执行文件即可。

输出中的数字表示:

  • 0000000000000000:部分中的地址。零是因为模板会自动放入其自己的部分
  • 0000000000000017:为其生成的代码的大小

我们可以通过以下方式更清楚地看到这一点:

objdump -S main.o | c++filt

结尾为:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

_ZN10MyTemplateIiE1fEiMyTemplate<int>::f(int)>决定不取消修改的c++filt的名字。

因此,我们看到为每个方法实例化都生成一个单独的节,并且每个节都占用了目标文件中的课程空间。

对象重新定义问题的解决方案

可以通过使用显式实例化和以下任一方法来避免此问题:

  • 在cpp文件上移动定义,仅在hpp上保留声明,即,将原始示例修改为:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    缺点:外部项目无法将模板与自己的类型一起使用。另外,您还必须显式实例化所有类型。但这可能是一个好处,因为从那时起程序员就不会忘记。

  • 在hpp上保留定义,并在每个includer上添加extern template,另请参见:using extern template (C++11),即,将原始示例修改为:

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    缺点:所有包含者都必须在其CPP文件中添加extern,程序员可能会忘记这样做。

  • 在hpp上保留定义,并在hpp上添加extern template以用于将要显式实例化的类型:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    缺点:您迫使外部项目执行自己的显式实例化。

使用这些解决方案之一,nm现在包含:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

所以我们看到只有mytemplate.o具有所需的MyTemplate<int>编译,而notmain.omain.o没有,因为U的含义不确定。 / p>

从包含的标头中删除定义,但还通过外部API公开模板

最后,当您想同时使用这两种情况时,还需要考虑另外一个用例:

  • 加快项目的编译速度
  • 将标头公开为外部库API,供其他人使用

要解决此问题,您可以执行以下任一操作:

    • mytemplate.hpp:模板定义
    • mytemplate_interface.hpp:模板声明仅与mytemplate_interface.hpp中的定义匹配,没有定义
    • mytemplate.cpp:包括mytemplate.hpp并进行明确的实例化
    • main.cpp以及代码库中的其他所有位置:包括mytemplate_interface.hpp,而不是mytemplate.hpp
    • mytemplate.hpp:模板定义
    • mytemplate_implementation.hpp:包括mytemplate.hpp并将extern添加到将要实例化的每个类
    • mytemplate.cpp:包括mytemplate.hpp并进行明确的实例化
    • main.cpp以及代码库中的其他所有位置:包括mytemplate_implementation.hpp,而不是mytemplate.hpp

甚至对于多个标头来说甚至更好:在intf文件夹内创建一个impl / includes/文件夹,并始终使用mytemplate.hpp作为名称。

mytemplate_interface.hpp方法如下:

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

编译并运行:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

输出:

2

在Ubuntu 18.04中测试。

C ++ 20模块

https://en.cppreference.com/w/cpp/language/modules

我认为此功能将在可用时提供最佳的设置,但我尚未对其进行检查,因为它在我的GCC 9.2.1中尚不可用。

您仍将必须进行显式实例化以获取加速/磁盘节省,但是至少我们将有一个合理的解决方案,“从包含的标头中删除定义,但还向模板公开外部API”,而无需在周围复制内容100次。

预期的用法(没有明确的指示,不确定确切的语法是什么样,请参见:How to use template explicit instantiation with C++20 modules?)是这样的:

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration

template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration

int main() {
    hello(1);
    hello("world");
}

然后在https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/

中提到了编译
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

因此,从中我们可以看到clang可以将模板接口+实现提取到魔术helloworld.pcm中,该魔术必须包含源代码How are templates handled in C++ module system?的一些LLVM中间表示形式,该中间表示仍可以进行模板规范

如何快速分析您的构建,看看它是否可以通过模板实例化获得很多好处

因此,您有一个复杂的项目,并且想确定模板实例化是否会带来重大收益,而无需实际进行完整的重构?

下面的分析可以通过借鉴以下My C++ object file is too big

的一些想法,来帮助您决定或至少选择最有希望的对象进行实验时首先进行重构。
# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

梦想:模板编译器缓存

我认为最终的解决方案是,如果我们可以使用:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

,然后myfile.o将自动在文件之间重用以前编译的模板。

除了向您的构建系统传递额外的CLI选项之外,这还意味着程序员要付出额外的0努力。

答案 3 :(得分:1)

这取决于编译器模型 - 显然有Borland模型和CFront模型。然后它还取决于你的意图 - 如果你正在编写一个库,你可能(如上所述)明确地实例化你想要的特化。

GNU c ++页面讨论了这里的模型https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html