내가 inline키워드를 명시적으로 쓰는 타입의 프로그래머인데, 링커에러가 뜬다? 그러면 이 글을 보시길 바란다.
3D자체엔진을 제작하려고 3D리소스 래핑을 하는 도중에 inline에 대하여 엄청난 오용을 하고 있었다는 사실을 깨달았다.
// Material.h
// FBX를 로드할 때 가져오는 정보.
class MaterialResource : public IGraphicsResource
{
using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
explicit MaterialResource(std::wstring_view _name);
virtual ~MaterialResource();
public:
inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture);
inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType);
private:
MaterialMapArray mMaterialMaps;
};
// Material.cpp
MaterialResource::MaterialResource(std::wstring_view _name)
: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
inline void MaterialResource::SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture)
{
mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture;
}
inline std::shared_ptr<Texture> MaterialResource::GetMaterialMap(eMaterialMapType _mapType)
{
return mMaterialMaps[static_cast<UINT>(_mapType)];
}
이런 클래스가 있다.
이 클래스는 일단 SetMaterialMap() 함수를 사용하면 에러를 이렇게 낸다.
그렇다. 그 악명높은 링커에러가 뜬다.
근데 정의와 구현부도 제대로 되어있고, 헤더가 꼬인거도 아니고....당연하게도 main문은 있다.
애초에 SetMaterialMap() 함수를 사용한 코드에서 나는 에러다. 해당 코드를 지우면 제대로 빌드가 된다.
뭐가 문제임? 하는 사람도 있을거고,
왜 이따구로 짬? 하는 사람도 있을거다...
일단 확실한건, 대부분은 "뭐가 문제임?" 혹은 "inline이 뭐임?" 하는 반응이 대부분일 것이다.
내가 아는 inline의 역할은 컴파일 단계에서 함수 호출을 없애고, 호출 위치에 함수의 코드를 그대로 붙여넣어 함수 호출비용을 줄이는 것으로 알고있었다.
물론 이것도 맞다. 하지만 보조적으로 한 가지 역할이 더 있다.
그건 바로 여러 파일에서 #include한 함수가 중복 정의되어도 괜찮다고 링커에게 알려주는 역할이라는 것이다......
약간 처음 들으면 애매할 수 있는데, 이해하고 보면 당연한 말이다.
이해를 돕기 위해 gpt쌤의 말을 가져오면,
inline을 쓰면 헤더 파일에서 구현해야 하는 이유는 컴파일러와 링커의 작동 방식 때문입니다. 이를 차근차근 설명드릴게요.
1. 컴파일러는 각 번역 단위에서 따로 작동
- C++에서 코드를 컴파일할 때 **각 소스 파일(.cpp)**은 독립적인 "번역 단위"로 처리됩니다.
- inline 함수는 컴파일러에게 "이 함수는 어디에서든 중복 정의될 수 있지만, 중복되어도 괜찮다"는 뜻을 전달합니다.
중요: inline 함수는 중복 정의를 허용하지만, 컴파일러는 이를 보장하기 위해 번역 단위마다 해당 함수의 정의를 알고 있어야 합니다.
2. 헤더 파일은 모든 번역 단위에 포함됨
- 헤더 파일에 inline 함수를 정의하면 이를 포함하는 모든 번역 단위에서 동일한 함수 정의가 생깁니다.
- 이 정의가 동일하다면, 링커는 이를 하나로 합쳐 문제가 없게 처리합니다.
예제:
- 이 코드가 포함된 각 .cpp 파일마다 Add 함수의 정의가 복사됩니다.
- 링커는 "이건 모두 동일한 함수다!"라고 인식하고 하나로 합칩니다.
3. CPP 파일에 정의하면 발생하는 문제
- inline 함수는 헤더 파일에 정의되지 않으면 다른 번역 단위에서 해당 정의를 볼 수 없습니다.
- 링커는 다른 번역 단위에서 함수의 정의를 찾을 수 없고, "이 기호를 찾을 수 없다"는 에러를 발생시킵니다.
문제 상황:
- 링커는 Add 함수가 헤더에 정의되어 있어야 한다고 가정하지만, 실제 정의는 CPP에 있으므로 충돌합니다.
4. inline의 의미와 동작
- inline의 의미: 함수가 호출될 때 **코드 삽입(inline expansion)**으로 대체될 수 있다.
- 컴파일러가 함수의 코드를 호출자 위치에 직접 삽입하려면, 함수의 **정의(body)**를 컴파일 시점에 알아야 합니다.
- 따라서 inline 함수는 반드시 모든 번역 단위에서 동일한 정의를 볼 수 있도록 헤더 파일에 구현해야 합니다.
5. 중복 정의를 피하기 위한 설계
- 일반적인 함수는 여러 번 정의되면 링커가 에러를 발생시킵니다(One Definition Rule 위반).
- 그러나 inline 함수는 특별히 "같은 정의라면 여러 번 있어도 괜찮다"는 규칙이 있습니다.
- 이 규칙을 제대로 활용하려면, 모든 번역 단위에서 동일한 정의가 필요하므로 헤더 파일에 구현해야 합니다.
요약: 헤더에 구현해야 하는 이유
- 모든 번역 단위에서 동일한 함수 정의를 보장하기 위해.
- 함수 호출 위치에 직접 코드 삽입하려면, 컴파일러가 함수의 정의를 알고 있어야 하기 때문에.
- 중복 정의로 인한 링킹 에러를 방지하기 위해.
따라서, inline 함수는 헤더 파일에 정의를 작성하고, CPP 파일에서는 구현을 배제해야 안정적으로 동작합니다! 😊
따라서, 쉽게 말하면 템플릿이랑 컴파일과정이 비슷하다고 볼 수 있습니다.
템플릿도 헤더에 구현을 하지 않으면 에러가 뜨므로.....
그래서 이렇게 바꾸니까 되었습니다.
class Texture;
// FBX를 로드할 때 가져오는 정보.
class MaterialResource : public IGraphicsResource
{
using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
explicit MaterialResource(std::wstring_view _name);
virtual ~MaterialResource();
public:
inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture) {
mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture;
}
inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType) {
return mMaterialMaps[static_cast<UINT>(_mapType)];
}
private:
MaterialMapArray mMaterialMaps;
};
MaterialResource::MaterialResource(std::wstring_view _name)
: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
MaterialState::MaterialState(MaterialResource* _pMaterialResource)
: mMaterialResource(_pMaterialResource)
{
for (int mapType = 0; mapType < static_cast<UINT>(eMaterialMapType::SIZE); ++mapType)
{
mCBuffer.UseMap[mapType] = TRUE;
}
}
cpp에 구현을 없애고 h에 구현을 했습니다.
근데 걍 느낀게....
처음에 inlline 굳이 쓴건, 어차피 컴파일러가 자동으로 inline화를 판단하지만, inline키워드를 씀으로써 const키워드처럼 필요없지만 (물론 const는 항상 필요없는건 아니다) 명시적으로 넣어주는 습관을 가지려고 쓴건데... 이렇게 복잡한 키워드인지 몰랐다.
하...걍 inline써도 컴파일러가 inline화 무시할 때도 있고, inline안써도 컴파일러가 알아서 inline화 시킬 때도 있는데,
걍 쓰지 맙시다.....
'프로그래밍 > C++' 카테고리의 다른 글
[C++] 2진 리터럴(Binary literals)에 대해 알아보자.araboza (C++14) (2) | 2024.09.16 |
---|---|
[C++] 오늘의 삽질 (0) | 2024.09.13 |
[C++] auto의 타입 추론에 대하여 알아보자.araboza (C++11) (0) | 2024.09.13 |
[C++] 복사 생성자(+복사 대입 연산자)를 알아보자.araboza (4) | 2024.09.12 |
[C++] is_base_of<> 특정 클래스를 상속 받았는지 확인하는 함수 (2) | 2024.09.08 |