명시 적 템플릿 인스턴스화-언제 사용됩니까? 템플릿에 대한 지식을 확장하고 확장 하려고합니다. 지금

몇 주 휴식 후, 저는 Templates – The Complete Guide by David Vandevoorde와 Nicolai M. Josuttis 책을 통해 템플릿에 대한 지식을 확장하고 확장 하려고합니다. 지금 이해하려는 것은 템플릿의 명시 적 인스턴스화입니다. .

실제로 메커니즘에 문제가있는 것은 아니지만이 기능을 사용하고 싶거나 사용하고 싶은 상황은 상상할 수 없습니다. 누군가 나에게 그것을 설명 할 수 있다면 나는 더 감사 할 것이다.



답변

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에 그 ++를 참조 할 필요가 컴파일러를.)


답변

몇 가지 명시 적 유형에 대해서만 작업하려는 템플릿 클래스를 정의하는 경우.

일반 클래스처럼 헤더 파일에 템플릿 선언을 넣으십시오.

일반 클래스처럼 소스 파일에 템플릿 정의를 넣습니다.

그런 다음 소스 파일의 끝에서 사용하려는 버전 만 명시 적으로 인스턴스화합니다.

어리석은 예 :

// 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();
}


답변

명시 적 인스턴스화를 통해 컴파일 시간과 개체 크기를 줄일 수 있습니다.

이것이 제공 할 수있는 주요 이점입니다. 아래 섹션에서 자세히 설명하는 다음 두 가지 효과에서 비롯됩니다.

  • 빌드 도구가 includer를 다시 빌드하지 못하도록 헤더에서 정의를 제거합니다.
  • 객체 재정의

헤더에서 정의 제거

명시 적 인스턴스화를 사용하면 .cpp 파일에 정의를 남길 수 있습니다.

정의가 헤더에 있고이를 수정하면 지능형 빌드 시스템이 모든 includer를 재 컴파일합니다. 이는 수십 개의 파일이 될 수 있으므로 컴파일 속도가 느려집니다.

.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 업스트림 .

다음을 사용하여 심볼 컴파일 및보기 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>가 두 파일 모두 에서 컴파일되었음을 의미 합니다.

여러 정의로 링크 타임에 폭발하지 않는 이유 는 링커가 여러 약한 정의를 허용하고 그중 하나를 선택하여 최종 실행 파일에 넣기 때문입니다.

출력의 숫자는 다음을 의미합니다.

  • 0000000000000000: 섹션 내 주소. 이 0은 템플릿이 자동으로 자체 섹션에 배치되기 때문입니다.
  • 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

_ZN10MyTemplateIiE1fEi의 변환 된 이름입니다 unmangle하지 않기로 결정했다.MyTemplate<int>::f(int)>c++filt

따라서 모든 단일 메소드 인스턴스화에 대해 별도의 섹션이 생성되고 각 섹션이 개체 파일의 공간을 차지한다는 것을 알 수 있습니다.

객체 재정의 문제에 대한 솔루션

이 문제는 명시 적 인스턴스화 및 다음 중 하나를 사용하여 방지 할 수 있습니다.

  • hpp에 정의를 유지하고 추가 extern template 명시 적으로 인스턴스화 할 유형에 대해 hpp를 .

    설명 된대로 : extern 템플릿 (C ++ 11)을 사용 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); }
    

    단점 :

    • 헤더 전용 라이브러리 인 경우 외부 프로젝트가 자신의 명시 적 인스턴스화를 수행하도록 강제합니다. 헤더 전용 라이브러리가 아닌 경우이 솔루션이 가장 좋습니다.
    • 템플릿 유형이 자체 프로젝트에 정의되어 있고 기본 제공되지 않는 int경우 헤더에 포함을 추가해야하는 것 같습니다. 포워드 선언만으로는 충분하지 않습니다. extern 템플릿 및 불완전한 유형 이로 인해 헤더 종속성이 증가합니다. 약간.
  • 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에 정의를 유지 extern template하고 모든 includer에 추가하십시오 .

    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); }
    

    단점 : 모든 포함자는 externCPP 파일에 를 추가해야 하므로 프로그래머가 잊을 가능성이 높습니다.

이러한 솔루션 중 하나를 사용하면 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.o하고 main.o있기 때문에하지 않는 U수단이 정의되지 않은.

포함 된 헤더에서 정의를 제거하고 헤더 전용 라이브러리의 외부 API에 템플릿을 노출합니다.

라이브러리가 헤더 전용이 아닌 경우 extern template프로젝트를 사용하면 명시 적 템플릿 인스턴스화의 개체를 포함하는 개체 파일에 연결되기 때문에 메서드가 작동합니다.

그러나 헤더 전용 라이브러리의 경우 둘 다 원하는 경우 :

  • 프로젝트 컴파일 속도 향상
  • 다른 사람이 사용할 수 있도록 헤더를 외부 라이브러리 API로 노출

다음 중 하나를 시도 할 수 있습니다.

    • mytemplate.hpp: 템플릿 정의
    • mytemplate_interface.hpp:의 정의와 만 일치하는 템플릿 선언, 정의 mytemplate_interface.hpp없음
    • mytemplate.cpp: mytemplate.hpp명시 적 인스턴스를 포함 하고 만듭니다.
    • main.cpp그리고 코드베이스의 다른 모든 곳 : include mytemplate_interface.hpp, notmytemplate.hpp
    • mytemplate.hpp: 템플릿 정의
    • mytemplate_implementation.hpp: 인스턴스화 될 모든 클래스를 포함 mytemplate.hpp하고 추가 extern합니다.
    • mytemplate.cpp: mytemplate.hpp명시 적 인스턴스를 포함 하고 만듭니다.
    • main.cpp그리고 코드베이스의 다른 모든 곳 : include mytemplate_implementation.hpp, notmytemplate.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에서 사용할 수 없기 때문에 아직 확인하지 않았습니다.

속도 향상 / 디스크 절약을 얻으려면 명시 적 인스턴스화를 수행해야하지만 적어도 100 번 정도 복사 할 필요가없는 “포함 된 헤더에서 정의를 제거하고 템플릿을 외부 API에 노출”에 대한 정상적인 솔루션이 있습니다.

예상되는 사용법 (명시적인 설명없이, 정확한 구문이 무엇인지 확실하지 않음, C ++ 20 모듈에서 템플릿 명시 적 인스턴스화를 사용하는 방법 참조 )는 다음과 같습니다.

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소스의 LLVM 중간 표현을 포함해야하는 magic으로 템플릿 인터페이스 + 구현을 추출 할 수 있음을 알 수 있습니다. 템플릿 은 C ++ 모듈 시스템에서 어떻게 처리됩니까? 여전히 템플릿 사양이 발생할 수 있습니다.

빌드를 빠르게 분석하여 템플릿 인스턴스화에서 많은 이점을 얻을 수 있는지 확인하는 방법

그렇다면 복잡한 프로젝트가 있고 템플릿 인스턴스화가 실제로 전체 리팩터링을 수행하지 않고도 상당한 이득을 가져올 지 결정하고 싶습니까?

아래 분석은 다음에서 몇 가지 아이디어를 빌려 실험하는 동안 가장 먼저 리팩토링 할 가장 유망한 객체를 결정하거나 적어도 선택하는 데 도움이 될 수 있습니다. 내 C ++ 객체 파일이 너무 큽니다.

# 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 옵션을 빌드 시스템에 전달하는 것 외에 프로그래머의 추가 노력이 없음을 의미합니다.

명시적인 템플릿 인스턴스화의 두 번째 보너스 : IDE가 템플릿 인스턴스화를 나열하는 데 도움이됩니다.

Eclipse와 같은 일부 IDE는 “사용 된 모든 템플릿 인스턴스화 목록”을 해결할 수 없음을 발견했습니다.

예를 들어, 템플릿 코드 내부에 있고 템플릿의 가능한 값을 찾으려면 생성자 사용법을 하나씩 찾아 가능한 유형을 하나씩 추론해야합니다.

그러나 Eclipse 2020-03에서는 클래스 이름에 대해 Find all usages (Ctrl + Alt + G) 검색을 수행하여 명시 적으로 인스턴스화 된 템플릿을 쉽게 나열 할 수 있습니다.

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

에:

template class AnimalTemplate<Dog>;

데모 : https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

IDE 외부에서 사용할 수있는 또 다른 게릴라 기술 nm -C은 최종 실행 파일에서 실행하고 템플릿 이름을 grep하는 것입니다.

nm -C main.out | grep AnimalTemplate

Dog인스턴스화 중 하나 라는 사실을 직접 가리 킵니다 .

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)


답변

컴파일러 모델에 따라 다릅니다. 분명히 Borland 모델과 CFront 모델이 있습니다. 그리고 그것은 또한 당신의 의도에 달려 있습니다. 만약 당신이 라이브러리를 작성한다면, 당신은 (위에서 언급했듯이) 당신이 원하는 전문화를 명시 적으로 인스턴스화 할 수 있습니다.

GNU c ++ 페이지는 https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html 여기에서 모델에 대해 설명합니다 .


답변