C++异常的幕后附录(完)

C++异常的幕后附录I:异常的真正代价

原文地址:https://monoinfinito.wordpress.com/2013/06/11/c-exceptions-under-the-hood-appendix-i-the-true-cost-of-an-exception/

作者:nicolasbrailo

还记得很久以前,当关于异常处理的系列文章刚刚开始时,我提到的这些文章只适用于gcc/x86吗?原因是不是所有的编译器都以相同的方式实现异常。特别的,有两个主要实现方式:

  • 使用一个查找表以及一些元数据,像Itanium ABI规定的;这是我们讨论的。
  • Sj/Lj(ARM):在进出一个方法时注册异常处理信息。

Gcc(以及许多其他编译器)在x86上实现这个ABI的方式是使用元数据(.gcc_except_table与CFI)。虽然它相当难解析,在运行时抛出异常时可能需要长时间解析,它有很大的好处:如果没有抛出异常,则无需设置的代价。这称为“零代价异常处理”,因为在没有异常抛出时,正常的执行无需付出代价。性能就像我们指明nothrow那样。这是正确的,把代码局部性与缓存问题放在一旁,不管是否使用异常,都不会影响性能,除非实际抛出异常。这是极大的优势,并且它符合c++的哲学,即不使用的特性没有代价。

在这些文章使用的程序中,在声明一个方法时使用noexcept说明(或者空的throw说明符,C++11以前),编译器将省略.gcc_except_table的生成。这使得代码更紧凑,这将改善缓存的使用,但这不太可能对应用程序的性能产生显著的影响。

如果我们讨论ARM,Sj/Lj看起来是缺省的选项(我相信这是有原因的,但我没有足够的ARM经验知道这一点)。这个异常处理方法基于在进出一个方法时注册的异常处理信息,如果抛出异常,这个方法使用异常或要求清理。这将导致更快的异常处理,但不管是否抛出异常都压迫付出代价。

如果你对sjlj以及零代价异常处理感兴趣,LLVM有很棒的文档

C++异常的幕后附录II:C++的元类与RTTI

原文地址:https://monoinfinito.wordpress.com/2013/06/13/c-exceptions-under-the-hood-appendix-ii-metaclasses-and-rtti-on-c/

作者:nicolasbrailo

很久以前,当我们正准备开始写我们无需libstdc++辅助处理异常的最小化ABI时,我们添加了一个空类来取悦链接器:

1

2

3

4

5

namespace __cxxabiv1 {

    struct __class_type_info {

        virtual void foo() {}

    } ti;

}

我提到这个类用于检查一个catch是否可以处理抛出异常的一个子类,但这又意味着什么呢?让我们对抛出函数稍作修改,看我们开始处理继承时发生什么。你可能想看这些例子的源代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

struct Derived_Exception : public Exception {};

 

void raise() {

    throw Derived_Exception();

}

 

void catchit() {

    try {

        raise();

    } catch(Exception&) {

        printf("Caught an Exception!\n");

    } catch(Derived_Exception&) {

        printf("Caught a Derived_Exception!\n");

    }

 

    printf("catchit handled the exception\n");

}

在这个例子里会发生什么是非常清楚的:它应该打印出“Catch an Exception”,因为catch块应该能够处理Exception与Derived_Exception。不仅如此,如果我们编译throw.cpp,我们将得到应该警告,让我们知道第二个catch是死代码:

1

2

3

throw.cpp: In function -F¡void catchit()¢:

throw.cpp:15:7: warning: exception of type ¡Derived_Exception¢ will be caught [enabled by default]

throw.cpp:13:7: warning:    by earlier handler for ¡Exception¢ [enabled by default]

幸好警告不会停止编译;我们可以继续并尝试链接结果的.o;我们将发现一个链接器错误:

  1. throw.o:(.rodata._ZTI17Derived_Exception[typeinfo for Derived_Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__si_class_type_info'

再次的,我们就是看到__type_info错误。如果我们创建一个假的__si_class_type_info来绕过这个问题,我们将最终看到在处理继承时我们的ABI以一个相当好玩的方式崩溃了:编译器警告我们死代码,然后我们看到我们的ABI执行相同的代码!

1

2

3

4

5

6

7

8

9

10

g++ -c -o throw.o -O0 -ggdb throw.cpp

throw.cpp: In function ¡void catchit()¢:

throw.cpp:15:7: warning: exception of type ¡Derived_Exception¢ will be caught [enabled by default]

throw.cpp:13:7: warning:    by earlier handler for ¡Exception¢ [enabled by default]

gcc main.o throw.o mycppabi.o -O0 -ggdb -o app

./app

begin FTW

Caught a Derived_Exception!

end FTW

catchit handled the exception

显然我们的ABI有问题,很容易就追踪回到can_handle的定义,检查异常是否可以被一个catch块捕捉的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

bool can_handle(const std::type_info *thrown_exception,

                const std::type_info *catch_type)

{

    // If the catch has no type specifier we're dealing with a catch(...)

    // and we can handle this exception regardless of what it is

    if (not catch_type) return true;

 

    // Naive type comparisson: only check if the type name is the same

    // This won't work with any kind of inheritance

    if (thrown_exception->name() == catch_type->name())

        return true;

 

    // If types don't match just don't handle the exception

    return false;

}

我们的ABI对要抛出的异常以及可以处理的类型获取std::type_info,然后比较这些类型的名字是否相同。这很好,只要不涉及继承,但在上面的例子里我们已经找到一个情形,即使名字不同,也应该处理异常。

在尝试捕捉一个异常指针时,出现相同的问题:名字不匹配。更有趣的,如果你尝试并链接throw.cpp,但修改catch接受一个指针,你将得到新的链接错误。如果修正它,你最终得到像这样的东西:

1

2

3

4

5

namespace __cxxabiv1 {

    struct __class_type_info    { virtual void foo() {} } ti;

    struct __si_class_type_info { virtual void foo() {} } si;

    struct __pointer_type_info  { virtual void foo() {} } ptr;

}

一个非常有趣的模式开始浮现:对每个可能使用的catch类型有不同的*_type_info。实际上,编译器对每个抛出风格生成不同的结构。例如,对这些抛出:

1

2

throw new Exception;

throw Exception;

编译器将生成像这样的东西:

1

2

__cxa_throw(_Struct_Type_Info__Ptr__Exception);

__cxa_throw(_Struct_Type_Info__Class__Exception);

实际上,即使对这个简单例子,继承网(不是树,是网)也是相当复杂的(注意我在这里创造了名字重整,它不是gcc所使用的):

所以这些类由编译器生成来准确说明哪些类要抛出,以及如何。例如,如果抛出一个类型为Ptr__Type_Info_Derived_Exception的异常,catch可以处理它,如果:

  • catch类型完全等于抛出类型(这是我们ABI进行的仅有检查)。
  • 如果catch类型是指针(即从cxxabi::pointer_type_info继承),并宣称该指针可以强制转换到异常类型。
  • 如果抛出类型是一个衍生类型,我们需要检查catch类型是否为父类型

这个列表仍然缺少了很多可能性,完整列表最好查看真正的C++ ABI。LLVM有非常清晰且容易理解的ABI。你可以在文件private_typeinfo.cpp里查看这些细节。如果查看LLVM运行时类型信息的实现,你将明白为什么我们不在我们的ABI上实现:确定两个类型是否相同的规则非常复杂。

C++异常的幕后附录III:RTTI与异常的正交性

原文地址:https://monoinfinito.wordpress.com/2013/07/25/c-exceptions-under-the-hood-appendix-iii-rtti-and-exceptions-orthogonality/

作者:nicolasbrailo

C++上异常处理要求大量的反射。我不是指程序员应该仔细思考异常处理(虽然这可能不是坏主意),我的意思是一段C++代码应该能够理解关于自身。这看起来非常像运行时类型信息,RTTI。它们是一样的吗?如果是,没有RTTI异常处理也能工作吗?

在编译我们的ABI项目时,通过在gcc上使用-fno-rtti,我们可能得到RTTI与异常处理区别的一点线索。让我们使用throw.cpp文件:

1

2

3

g++ -fno-rtti -S throw.cpp -o throw.nortti.s

g++ -S throw.cpp -o throw.s

diff throw.s throw.nortti.s

如果你自己尝试你应该看到RTTI与非RTTI版本没有区别。我们可以下结论说gcc的异常处理机制异于RTTI吗?还不行,让我们看一下,如果尝试使用RTTI会发生什么:

1

2

3

4

5

void raise() {

    Exception ex;

    typeid(ex);

    throw Exception();

}

如果你尝试编译这,gcc将抱怨:你不能使用-fno-rtti又使用typeid。这是合理的。让我们以简单的测试看一下typeid做什么:

1

2

3

4

5

6

7

8

#include <typeinfo>

 

class Bar {};

const std::type_info& foo()

{

        Bar bar;

            return typeid(bar);

}

如果我们使用g++ -O0 -S编译此,你将看到foo被编译为像这样的东西:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

_Z3foov:

.LFB19:

    # Prologue stuff...

 

    subl    $16, %esp

    # Bar bar

 

    movl    $_ZTI3Bar, %eax

    # typeid(bar)

 

    leave

    # Epilogue stuff...

 

_ZTS3Bar:

    # Definition for _ZTS3Bar...

 

_ZTI3Bar:

    .long   _ZTVN10__cxxabiv117__class_type_infoE+8

    .long   _ZTS3Bar

    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"

    .section    .note.GNU-stack,"",@progbits

看起来熟悉吗?如果不是,那么尝试把样例代码改为这个:

1

2

class Bar {};

void foo() { throw Bar(); }

像g++ -O1 -fnot-rtti -S test.cpp这样编译它,查看结果文件。现在你应该看到像这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

_Z3foov:

    # Prologue stuff...

 

    # Initialize exception

    subl    $24, %esp

    movl    $1, (%esp)

    call    __cxa_allocate_exception

    movl    $0, 8(%esp)

 

    # Specify Bar as exception thrown

    movl    $_ZTI3Bar, 4(%esp)

    movl    %eax, (%esp)

 

    # Handle exception

    call    __cxa_throw

 

    # Epilogue stuff...

 

_ZTS3Bar:

    # Definition for _ZTS3Bar...

 

_ZTI3Bar:

    .long   _ZTVN10__cxxabiv117__class_type_infoE+8

    .long   _ZTS3Bar

    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"

    .section    .note.GNU-stack,"",@progbits

这确实看起来熟悉:要抛出的类完全与用于typeid的类相同!

现在我们可以总结发生了什么:异常抛出类型信息实现,需要反射并依赖于它的RTTI信息,与typeid以及其他RTTI元素底下的实现完全相同。在g++上指明-fno-rtti仅禁止了RTTI的“前端”方法:这意味着你将不能使用typeid,不会生成RTTI类……除非抛出一个异常,在这个情形里将生成所需的RTTI类,不管-fno-rtti是否出现(虽然你仍然不能通过typeid访问这个类的RTTI信息)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值