admin管理员组

文章数量:1029732

万能的前向声明碰到他竟然不行了

您好,我是昊天,国内某头部音频公司的C++主程,多年的音视频开发经验,熟悉Qt、FFmpeg、OpenGL。如果你对这些感兴趣,欢迎关注我的公众号

最近遇到一个问题,可简化如下:汽车类存在一个引擎实例,由于汽车内只有一个引擎且不涉及共享所有权问题,所以使用unique_ptr来管理。但是按照如下的写作方式,编译报错。

代码语言:javascript代码运行次数:0运行复制
//car.h
class Engine;
class Car
{
public:
//;;;
private:
    std::unique_ptr<Engine> m_engine{nullptr};
};

由上代码可知,我在Car类中使用了Engine类的前向声明,但是在Car类中使用unique_ptr来管理Engine类的实例时,编译报错。

如上为本文所要讨论的问题,不着急,且让我们娓娓道来。

1. 前向声明

C++中的前向声明是一种可以在不完全定义一个类型的情况下告诉编译器“这个类型存在”的声明方式。可用于类、结构体、函数等。前向声明最大的好处是可以减少编译依赖、降低耦合,从而提高编译速度。但是前向声明有一个限制——凡是需要具体实现的地方都不能用。所以不能通过前向声明定义变量、调用函数。但是针对于指针、引用,个人还是推荐使用前向声明的。

2. 智能指针

C++11引入了智能指针,其借助RAII机制来管理动态分配的内存,避免内存泄漏。智能指针主要包括unique_ptr、shared_ptr、weak_ptr等。

  • • unique_ptr:独占式智能指针,保证同一时间只有一个智能指针指向该对象。
  • • shared_ptr:共享式智能指针,允许多个智能指针指向同一对象,使用引用计数来管理对象的生命周期。
  • • weak_ptr:弱引用智能指针,不影响对象的生命周期,用于解决shared_ptr的循环引用问题。

如上三条虽是八股套路,但据此仍可知道各个类型指针的使用场景。但是,目前的项目代码中好像存在一个不好的风气——只要不会发生循环引用,就用shared_ptr。不产生循环引用的shared_ptr的貌似真的成了万金油,简单易用,还不会发生内存泄漏。这也就导致了shared_ptr的滥用。 但是share_ptr真如预想的那么好吗?其也存在性能影响:

  • 内存占用:shared_ptr的尺寸是裸指针的2倍,其既包含一个裸指针,又包含一个指涉到引用计数的裸指针。
  • 引用计数的内存必须动态分配:shared_ptr需要记录被托管对象的引用计数,该引用计数作为动态分配的数据存储。
  • 引用计数的递增递减是原子的:shared_ptr的引用计数是线程安全的,为此,shared_ptr的引用计数的递增递减必须是原子的。

注:共享指针的引用计数是线程安全的,但是共享指针的托管对象不是线程安全的。

基于如上的分析,在项目中,使用智能指针时要考虑清楚,避免滥用。

建议:涉及到指针选型时,首先要考虑使用unique_ptr(因为它又快又小);必须要找到足够的理由后舍弃unique_ptr用shared_ptr;最后才是weak_ptr。

3. unique_ptr不能用前向声明

结合如上信息,前向声明可以用声明指针的场景;考虑到内存占用和性能影响,选用unique_ptr;但是如最开头的示例代码——unique_ptr使用前向声明时编译器报错。

代码语言:javascript代码运行次数:0运行复制
//error info as follows:
error C2027: use of undefined type 'ClassName'
  ---------
see reference to classtemplate instantiation 'std::default_delete<ClassName>' being compiled
[build]           C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.43.34808\include\memory(3297,23):
[build]           while compiling classtemplate member function 'void std::default_delete<ClassName>::operator ()(_Ty *) noexcept const'
[build]           with
[build]           [
[build]               _Ty=ClassName
[build]           ]
[build]               C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.43.34808\include\memory(3409,33):
[build]               see the first reference to 'std::default_delete<ClassName>::operator ()' in 'std::unique_ptr<ClassName,std::default_delete<ClassName>>::~unique_ptr'
[build]               E:\rocky_work\wanosgameengine\wanosGameEngine\private\core\WanosRender\WanosRenderDoubleEar.cpp(30,28):
[build]               see the first reference to 'std::unique_ptr<ClassName,std::default_delete<ClassName>>::~unique_ptr' in 'xxxx'
error C2338: static_assert failed: 'can't delete an incomplete type'

如上提示为:在unique_ptr的析构函数中,需要知道被管理对象的完整类型——这也正是前向声明不适用的场景。

可是,为什么unique_ptr需要被管理对象的完整类型呢?为什么shared_ptr不会出现如此的问题呢

分析源码

要分析如上问题,唯一的方案就是看源码了,源码面前无秘密。 unique_ptr和shared_ptr的的类型定义分别如下:

代码语言:javascript代码运行次数:0运行复制
_EXPORT_STD template <class_Ty, class_Dx/* = default_delete<_Ty> */>
classunique_ptr {

};

_EXPORT_STD template <class_Ty>
structdefault_delete { // default deleter for unique_ptr
    constexpr default_delete() noexcept = default;

    template <class_Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0>
    _CONSTEXPR23 default_delete(const default_delete<_Ty2>&) noexcept {}

    _CONSTEXPR23 voidoperator()(_Ty* _Ptr) constnoexcept/* strengthened */ { // delete a pointer
        static_assert(0 < sizeof(_Ty), "can't delete an incomplete type");//这句话是不是很熟悉,就是如上的报错
        delete _Ptr;
    }
};




_EXPORT_STD template <class_Ty>
classshared_ptr : public _Ptr_base<_Ty> { 

};

template <class_Ty>
class_Ptr_base {

}

通过如上的代码可知:

  • • shared_ptr和unique_ptr均是模板类,但是unique_ptr的模板参数有两个,而shared_ptr的模板参数只有一个。
  • • 对于unique_ptr第二个参数定义的析构器是unique_ptr的一部分,如果没有指定析构器时,使用默认的default_delete<_Ty>。而默认的析构器需要调用delete _Ptr 方法,此时是需要知晓对象的完整类型的。所以unique_ptr无法使用前向声明。
  • • 而对于shared_ptr,其析构器作为控制块的一部分,并不是指针的一部分。所以shared_ptr无需知晓对象的完整类型,故其可以使用前向声明

进一步的

  • • 同样是类A的unique_ptr指针,定义不同的析构器时,其本质两个不同的类型;
  • • 同样是类A的shared_ptr指针,定义不同的析构器时,其仍为同一种类型,进一步的,不定义析构器,定义析构器A、析构器B,其本质为同一种类型。

总结

在声明指针时,使用前向声明可以减少编译依赖,降低耦合,提高编译速度。本文分析了为什么unique_ptr无法使用前向声明,并结合shared_ptr的源码分析,说明了为什么shared_ptr可以使用前向声明。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-18,如有侵权请联系 cloudcommunity@tencent 删除内存指针unique编译对象

万能的前向声明碰到他竟然不行了

您好,我是昊天,国内某头部音频公司的C++主程,多年的音视频开发经验,熟悉Qt、FFmpeg、OpenGL。如果你对这些感兴趣,欢迎关注我的公众号

最近遇到一个问题,可简化如下:汽车类存在一个引擎实例,由于汽车内只有一个引擎且不涉及共享所有权问题,所以使用unique_ptr来管理。但是按照如下的写作方式,编译报错。

代码语言:javascript代码运行次数:0运行复制
//car.h
class Engine;
class Car
{
public:
//;;;
private:
    std::unique_ptr<Engine> m_engine{nullptr};
};

由上代码可知,我在Car类中使用了Engine类的前向声明,但是在Car类中使用unique_ptr来管理Engine类的实例时,编译报错。

如上为本文所要讨论的问题,不着急,且让我们娓娓道来。

1. 前向声明

C++中的前向声明是一种可以在不完全定义一个类型的情况下告诉编译器“这个类型存在”的声明方式。可用于类、结构体、函数等。前向声明最大的好处是可以减少编译依赖、降低耦合,从而提高编译速度。但是前向声明有一个限制——凡是需要具体实现的地方都不能用。所以不能通过前向声明定义变量、调用函数。但是针对于指针、引用,个人还是推荐使用前向声明的。

2. 智能指针

C++11引入了智能指针,其借助RAII机制来管理动态分配的内存,避免内存泄漏。智能指针主要包括unique_ptr、shared_ptr、weak_ptr等。

  • • unique_ptr:独占式智能指针,保证同一时间只有一个智能指针指向该对象。
  • • shared_ptr:共享式智能指针,允许多个智能指针指向同一对象,使用引用计数来管理对象的生命周期。
  • • weak_ptr:弱引用智能指针,不影响对象的生命周期,用于解决shared_ptr的循环引用问题。

如上三条虽是八股套路,但据此仍可知道各个类型指针的使用场景。但是,目前的项目代码中好像存在一个不好的风气——只要不会发生循环引用,就用shared_ptr。不产生循环引用的shared_ptr的貌似真的成了万金油,简单易用,还不会发生内存泄漏。这也就导致了shared_ptr的滥用。 但是share_ptr真如预想的那么好吗?其也存在性能影响:

  • 内存占用:shared_ptr的尺寸是裸指针的2倍,其既包含一个裸指针,又包含一个指涉到引用计数的裸指针。
  • 引用计数的内存必须动态分配:shared_ptr需要记录被托管对象的引用计数,该引用计数作为动态分配的数据存储。
  • 引用计数的递增递减是原子的:shared_ptr的引用计数是线程安全的,为此,shared_ptr的引用计数的递增递减必须是原子的。

注:共享指针的引用计数是线程安全的,但是共享指针的托管对象不是线程安全的。

基于如上的分析,在项目中,使用智能指针时要考虑清楚,避免滥用。

建议:涉及到指针选型时,首先要考虑使用unique_ptr(因为它又快又小);必须要找到足够的理由后舍弃unique_ptr用shared_ptr;最后才是weak_ptr。

3. unique_ptr不能用前向声明

结合如上信息,前向声明可以用声明指针的场景;考虑到内存占用和性能影响,选用unique_ptr;但是如最开头的示例代码——unique_ptr使用前向声明时编译器报错。

代码语言:javascript代码运行次数:0运行复制
//error info as follows:
error C2027: use of undefined type 'ClassName'
  ---------
see reference to classtemplate instantiation 'std::default_delete<ClassName>' being compiled
[build]           C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.43.34808\include\memory(3297,23):
[build]           while compiling classtemplate member function 'void std::default_delete<ClassName>::operator ()(_Ty *) noexcept const'
[build]           with
[build]           [
[build]               _Ty=ClassName
[build]           ]
[build]               C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.43.34808\include\memory(3409,33):
[build]               see the first reference to 'std::default_delete<ClassName>::operator ()' in 'std::unique_ptr<ClassName,std::default_delete<ClassName>>::~unique_ptr'
[build]               E:\rocky_work\wanosgameengine\wanosGameEngine\private\core\WanosRender\WanosRenderDoubleEar.cpp(30,28):
[build]               see the first reference to 'std::unique_ptr<ClassName,std::default_delete<ClassName>>::~unique_ptr' in 'xxxx'
error C2338: static_assert failed: 'can't delete an incomplete type'

如上提示为:在unique_ptr的析构函数中,需要知道被管理对象的完整类型——这也正是前向声明不适用的场景。

可是,为什么unique_ptr需要被管理对象的完整类型呢?为什么shared_ptr不会出现如此的问题呢

分析源码

要分析如上问题,唯一的方案就是看源码了,源码面前无秘密。 unique_ptr和shared_ptr的的类型定义分别如下:

代码语言:javascript代码运行次数:0运行复制
_EXPORT_STD template <class_Ty, class_Dx/* = default_delete<_Ty> */>
classunique_ptr {

};

_EXPORT_STD template <class_Ty>
structdefault_delete { // default deleter for unique_ptr
    constexpr default_delete() noexcept = default;

    template <class_Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0>
    _CONSTEXPR23 default_delete(const default_delete<_Ty2>&) noexcept {}

    _CONSTEXPR23 voidoperator()(_Ty* _Ptr) constnoexcept/* strengthened */ { // delete a pointer
        static_assert(0 < sizeof(_Ty), "can't delete an incomplete type");//这句话是不是很熟悉,就是如上的报错
        delete _Ptr;
    }
};




_EXPORT_STD template <class_Ty>
classshared_ptr : public _Ptr_base<_Ty> { 

};

template <class_Ty>
class_Ptr_base {

}

通过如上的代码可知:

  • • shared_ptr和unique_ptr均是模板类,但是unique_ptr的模板参数有两个,而shared_ptr的模板参数只有一个。
  • • 对于unique_ptr第二个参数定义的析构器是unique_ptr的一部分,如果没有指定析构器时,使用默认的default_delete<_Ty>。而默认的析构器需要调用delete _Ptr 方法,此时是需要知晓对象的完整类型的。所以unique_ptr无法使用前向声明。
  • • 而对于shared_ptr,其析构器作为控制块的一部分,并不是指针的一部分。所以shared_ptr无需知晓对象的完整类型,故其可以使用前向声明

进一步的

  • • 同样是类A的unique_ptr指针,定义不同的析构器时,其本质两个不同的类型;
  • • 同样是类A的shared_ptr指针,定义不同的析构器时,其仍为同一种类型,进一步的,不定义析构器,定义析构器A、析构器B,其本质为同一种类型。

总结

在声明指针时,使用前向声明可以减少编译依赖,降低耦合,提高编译速度。本文分析了为什么unique_ptr无法使用前向声明,并结合shared_ptr的源码分析,说明了为什么shared_ptr可以使用前向声明。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-18,如有侵权请联系 cloudcommunity@tencent 删除内存指针unique编译对象

本文标签: 万能的前向声明碰到他竟然不行了