封装:public 成员 vs public 方法

Encapsulation: public member vs public method

假设我有一个名为 PointerSet<T> 的 class。它本质上是一个 vector,通过使用 std::lower_bound 就像 set 一样(但它是如何工作的并不重要,所以我不会详细介绍)。

template<typename T>
class PointerSet
{
    ...

public:
    void insert(T&);
    void erase(T&);
    void erase(const std::vector<T>&);
};

"function tunnel" 会影响性能吗? (我不知道专业术语,但它是我心目中的称呼。它是一个函数调用一个具有完全相同目的的函数。)请参阅版本 1.

版本 1:创建成员方法

...

template<typename T>
class Node
{
    PointerSet<T> Links;
public:
    void insertLink(T& p){ Links.insert(p); }
    void eraseLink(T& p){ Links.erase(p); }
    void eraseLink(const std::vector<T>& p){ Links.erase(p); }
};

class bar;
class foo : public Node<bar> { };    // now foo can insert links to bars.

...

版本 2:只需使用 public 成员

...

class bar;
struct foo
{
    PointerSet<bar> Links;    // Use Links directly
};

以下哪种方法最好?我正在考虑性能 易于调试。

除非您想限制可以对该数据成员执行的操作,否则我建议使用 public 成员。如果您所做的只是用您自己的方法名称包装方法,那么将它们放在那里是没有意义的。

  • 通常,如果不遵守 IS-A 关系,您应该避免 public 推导,因为它会导致该组合的耦合度更高。
  • 您还应该避免使用 public 成员,因为它会使应用程序将来更难维护。
  • 如果您可以在第二个示例中直接使用 PointerSet,那么在版本 1 中,您也可以直接从 PointerSet 派生。这样比较是不公平的。

在实践中,可接受的妥协在很大程度上取决于每个 class 的实际目的。 如果额外的间接寻址没有带来任何好处(减少编译),它们将毫无用处依赖项、access/visibility 控制、额外功能、代码重用机会...)

Will having a "function tunnel" take a hit to performance?

很可能不会。特别是因为你在这里处理模板,所以定义都是可见的并且很容易被编译器内联。看看这个link:https://godbolt.org/g/cWR7N3

我所做的就是编译这两个代码片段。首先,从节点 class.

调用函数
#include <vector>

// these functions are not defined, so that the compiler
// cannot inline them or optimize them out
void insert_impl(void const*);
void erase_impl(void const*);
void erase_impl_vec(void const*);

template<typename T>
class PointerSet
{
public:
    void insert(T& v) { insert_impl(&v); }
    void erase(T& v) { erase_impl(&v); }
    void erase(const std::vector<T>& v) {
        erase_impl_vec(&v);
    }
};

template<typename T>
class Node
{
    PointerSet<T> Links;
public:
    void insertLink(T& p){ Links.insert(p); }
    void eraseLink(T& p){ Links.erase(p); }
    void eraseLink(const std::vector<T>& p){ Links.erase(p); }
};

int main()
{
    Node<int> n;

    int x;
    n.insertLink(x);
    n.eraseLink(x);

    std::vector<int> v;
    n.eraseLink(v);
}

然后直接从 PointerSet class.

调用它们
#include <vector>

// these functions are not defined, so that the compiler
// cannot inline them or optimize them out
void insert_impl(void const*);
void erase_impl(void const*);
void erase_impl_vec(void const*);

template<typename T>
class PointerSet
{
public:
    void insert(T& v) { insert_impl(&v); }
    void erase(T& v) { erase_impl(&v); }
    void erase(const std::vector<T>& v) {
        erase_impl_vec(&v);
    }
};

int main()
{
    PointerSet<int> n;

    int x;
    n.insert(x);
    n.erase(x);

    std::vector<int> v;
    n.erase(v);
}

如您在 link (https://godbolt.org/g/cWR7N3) 中所见,编译器为每个输出相同的程序集。

main:                                   # @main
        push    rbx
        sub     rsp, 48
        lea     rbx, [rsp + 12]
        mov     rdi, rbx
        call    insert_impl(void const*)
        mov     rdi, rbx
        call    erase_impl(void const*)
        xorps   xmm0, xmm0
        movaps  xmmword ptr [rsp + 16], xmm0
        mov     qword ptr [rsp + 32], 0
        lea     rdi, [rsp + 16]
        call    erase_impl_vec(void const*)
        mov     rdi, qword ptr [rsp + 16]
        test    rdi, rdi
        je      .LBB0_3
        call    operator delete(void*)
.LBB0_3:
        xor     eax, eax
        add     rsp, 48
        pop     rbx
        ret
        mov     rbx, rax
        mov     rdi, qword ptr [rsp + 16]
        test    rdi, rdi
        je      .LBB0_6
        call    operator delete(void*)
.LBB0_6:
        mov     rdi, rbx
        call    _Unwind_Resume
GCC_except_table0:
        .byte   255                     # @LPStart Encoding = omit
        .byte   3                       # @TType Encoding = udata4
        .byte   41                      # @TType base offset
        .byte   3                       # Call site Encoding = udata4
        .byte   39                      # Call site table length
        .long   .Lfunc_begin0-.Lfunc_begin0 # >> Call Site 1 <<
        .long   .Ltmp0-.Lfunc_begin0    #   Call between .Lfunc_begin0 and .Ltmp0
        .long   0                       #     has no landing pad
        .byte   0                       #   On action: cleanup
        .long   .Ltmp0-.Lfunc_begin0    # >> Call Site 2 <<
        .long   .Ltmp1-.Ltmp0           #   Call between .Ltmp0 and .Ltmp1
        .long   .Ltmp2-.Lfunc_begin0    #     jumps to .Ltmp2
        .byte   0                       #   On action: cleanup
        .long   .Ltmp1-.Lfunc_begin0    # >> Call Site 3 <<
        .long   .Lfunc_end0-.Ltmp1      #   Call between .Ltmp1 and .Lfunc_end0
        .long   0                       #     has no landing pad
        .byte   0                       #   On action: cleanup