2

这是一个有点宽泛的问题,似乎没有一个真正的答案。

很长一段时间以来,我一直对组合对象的初始化感到困惑。我被正式教导为所有成员数据提供 getter 和 setter,并支持指向对象的原始指针而不是自动对象 - 这似乎与 Stack Overflow 上许多人(​​例如这篇受欢迎的帖子)的建议形成鲜明对比。

那么,我应该如何初始化对象组合对象呢?

这是我尝试使用在学校学到的东西进行初始化的方式:

class SmallObject1 {
public:
    SmallObject1() {};
};

class SmallObject2 {
    public:
        SmallObject2() {};
};

class BigObject {
    private:
        SmallObject1 *obj1;
        SmallObject2 *obj2;
        int field1;
        int field2;
    public:
        BigObject() {}
        BigObject(SmallObject1* obj1, SmallObject2* obj2, int field1, int field2) {
        // Assign values as you would expect
        }
        ~BigObject() {
            delete obj1;
            delete obj2;
        }
    // Apply getters and setters for ALL members here
};

int main() {
    // Create data for BigObject object
    SmallObject1 *obj1 = new SmallObject1();
    SmallObject2 *obj2 = new SmallObject2();
    int field1 = 1;
    int field2 = 2;

    // Using setters
    BigObject *bobj1 = new BigObject();
    // Set obj1, obj2, field1, field2 using setters

    // Using overloaded contructor
    BigObject *bobj2 = new BigObject(obj1, obj2, field1, field2);

    return 0;
}

这个设计很吸引人,因为它是可读的(对我来说)。BigObject具有指向其成员对象的指针的事实使得初始化obj1obj2初始化之后成为可能。然而,动态内存可能会使程序在未来变得更加复杂和混乱,因此内存泄漏的时机已经成熟。此外,getter 和 setter 的使用使类变得混乱,并且还可能使成员数据太容易访问和改变。

这实际上是不好的做法吗?我经常发现需要将成员对象与其所有者分开初始化的时候,这使得自动对象没有吸引力。此外,我考虑过让较大的对象构造它们自己的成员对象。从安全的角度来看,这似乎更有意义,但从对象责任的角度来看,意义不大。

4

4 回答 4

0

考虑以下代码:

class SmallObj {
public:
  int i_;
  double j_;
  SmallObj(int i, double j) : i_(i), j_(j) {}
};

class A {
  SmallObj so_;
  int x_;
public:
  A(SmallObj so, int x) : so_(so), x_(x) {}
  int something();
  int sox() const { return so_.i_; }
};

class B {
  SmallObj* so_;
  int x_;
public:
  B(SmallObj* so, int x) : so_(so), x_(x) {}
  ~B() { delete so_; }
  int something();
  int sox() const { return so_->i_; }
};

int a1() {
  A mya(SmallObj(1, 42.), -1.);
  mya.something();
  return mya.sox();
}

int a2() {
  SmallObj so(1, 42.);
  A mya(so, -1.);
  mya.something();
  return mya.sox();
}

int b() {
  SmallObj* so = new SmallObj(1, 42.);
  B myb(so, -1.);
  myb.something();
  return myb.sox();
}

方法“A”的缺点:

  • 我们的具体使用SmallObject使我们依赖于它的定义:我们不能只是向前声明它,
  • 我们的实例对我们的实例SmallObject来说是唯一的(不共享),

接近“B”的缺点有几个:

  • 我们需要建立所有权合同并让用户知道它,
  • 必须在B创建之前执行动态内存分配,
  • 需要间接访问这个重要对象的成员,
  • 如果我们要支持您的默认构造函数案例,我们必须测试空指针,
  • 破坏需要进一步的动态内存调用,

反对使用自动对象的论据之一是按值传递它们的成本

这是可疑的:在许多琐碎的自动对象的情况下,编译器可以针对这种情况进行优化并在线初始化子对象。如果构造函数是微不足道的,它甚至可以在一个堆栈初始化中完成所有事情。

这是 GCC 对 a1() 的 -O3 实现

_Z2a1v:
.LFB11:
  .cfi_startproc
  .cfi_personality 0x3,__gxx_personality_v0
  subq  $40, %rsp      ; <<
  .cfi_def_cfa_offset 48
  movabsq $4631107791820423168, %rsi  ; <<
  movq  %rsp, %rdi     ; <<
  movq  %rsi, 8(%rsp)  ; <<
  movl  $1, (%rsp)     ; <<
  movl  $-1, 16(%rsp)  ; <<
  call  _ZN1A9somethingEv
  movl  (%rsp), %eax
  addq  $40, %rsp
  .cfi_def_cfa_offset 8
  ret
  .cfi_endproc

突出显示的 ( ; <<) 行是编译器在一次就地构建 A 和它的 SmallObj 子对象。

a2() 的优化非常相似:

_Z2a2v:
.LFB12:
  .cfi_startproc
  .cfi_personality 0x3,__gxx_personality_v0
  subq  $40, %rsp
  .cfi_def_cfa_offset 48
  movabsq $4631107791820423168, %rcx
  movq  %rsp, %rdi
  movq  %rcx, 8(%rsp)
  movl  $1, (%rsp)
  movl  $-1, 16(%rsp)
  call  _ZN1A9somethingEv
  movl  (%rsp), %eax
  addq  $40, %rsp
  .cfi_def_cfa_offset 8
  ret
  .cfi_endproc

还有 b():

_Z1bv:
.LFB16:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        .cfi_lsda 0x3,.LLSDA16
        pushq   %rbx
        .cfi_def_cfa_offset 16
        .cfi_offset 3, -16
        movl    $16, %edi
        subq    $16, %rsp
        .cfi_def_cfa_offset 32
.LEHB0:
        call    _Znwm
.LEHE0:
        movabsq $4631107791820423168, %rdx
        movl    $1, (%rax)
        movq    %rsp, %rdi
        movq    %rdx, 8(%rax)
        movq    %rax, (%rsp)
        movl    $-1, 8(%rsp)
.LEHB1:
        call    _ZN1B9somethingEv
.LEHE1:
        movq    (%rsp), %rdi
        movl    (%rdi), %ebx
        call    _ZdlPv
        addq    $16, %rsp
        .cfi_remember_state
        .cfi_def_cfa_offset 16
        movl    %ebx, %eax
        popq    %rbx
        .cfi_def_cfa_offset 8
        ret
.L6:
        .cfi_restore_state
.L3:
        movq    (%rsp), %rdi
        movq    %rax, %rbx
        call    _ZdlPv
        movq    %rbx, %rdi
.LEHB2:
        call    _Unwind_Resume
.LEHE2:
        .cfi_endproc

显然,在这种情况下,我们付出了沉重的代价来传递指针而不是值。

现在让我们考虑以下代码:

class A {
    SmallObj* so_;
public:
    A(SmallObj* so);
    ~A();
};

class B {
    Database* db_;
public:
    B(Database* db);
    ~B();
};

从上面的代码中,您对 A 的构造函数中“SmallObj”的所有权的期望是什么?您对 B 中“数据库”的所有权的期望是什么?您是否打算为您创建的每个 B 构建一个唯一的数据库连接?

为了进一步回答您偏爱原始指针的问题,我们只需要查看 2011 C++ 标准,该标准引入了std::unique_ptrstd::shared_ptr帮助解决自 Cs 以来存在的所有权歧义strdup()(返回指向字符串副本的指针,记得释放)。

标准委员会提出了一个observer_ptr在 C++17 中引入的提议,它是一个围绕原始指针的非拥有包装器。

将这些与您喜欢的方法一起使用会引入很多样板:

auto so = std::make_unique<SmallObject>(1, 42.);
A a(std::move(so), -1);

我们在这里知道我们分配a的实例的所有权so,因为我们通过明确授予它所有权std::move。但是所有这些都是明确的成本字符。对比:

A a(SmallObject(1, 42.), -1);

或者

SmallObject so(1, 4.2);
A a(so, -1);

所以我认为总的来说,很少有情况支持小对象的原始指针进行组合。您应该查看您的材料以得出结论,因为您似乎在何时使用原始指针的建议中忽略或误解了因素。

于 2016-07-08T21:25:57.017 回答
0

其他人已经描述了优化原因,我现在从类型/功能的角度来看。根据 Stroustrup 的说法,“建立类不变量是每个构造函数的工作”。你的类不变量是什么?知道(和定义!)很重要,否则你会用ifs 污染你的成员函数来检查操作是否有效——这并不比没有类型好多少。在 90 年代我们有这样的类,但现在我们真的坚持不变的定义,并希望对象一直处于有效状态。(函数式编程更进一步,尝试从对象中提取变量状态,因此对象可以是 const。)

  • 如果你的类是有效的,如果你有这些子对象,那么把它们作为成员,句号。
  • 如果你想在 BigObjects 之间共享SmallObjects,那么你需要指针。
  • 如果没有给定的 SmallObject 是有效的,但您不需要共享,则可以考虑std::optional<SmallObject>成员。Optional 通常在本地分配(与堆相比),因此您可能会受益于缓存局部性。
  • 如果你发现构造这样一个对象很困难,例如构造函数参数太多,那么你有两个正交问题:构造和类成员。通过引入构建器类(构建器模式)来解决构建问题。通常可行的解决方案是将所有构造函数的所有参数作为可选成员。

请注意,我们中许多喜欢函数式风格的人认为 builder 是一种反模式,并且仅将其用于反序列化(如果有的话)。背后的原因,很难推断一个 builer (结果是什么,它会成功,哪个构造函数被调用)。如果你有两个整数,那就是:两个整数。您最好的选择通常是将它们保存在单独的变量中,然后由编译器进行各种优化。如果这些碎片奇迹般地落下并且您的 int 将“就地”构建,我不会感到惊讶,因此以后不需要复制。

OTOH,如果您发现相同的参数在许多地方先于其他地方“得到限制”(获取它们的值),那么您可能会为它们引入一种类型。在这种情况下,您的两个整数将是一种类型(最好是结构)。您可能会决定是否要将其设为 的基类BigObject、成员或只是一个单独的类(如果您有多个绑定顺序,则必须选择第三个) - 在任何一种情况下,您的构造函数现在都将采用新的类而不是两个整数。您甚至可以考虑将其他构造函数(采用两个整数的构造函数)作为 1. 新对象可以轻松构造,2. 它可能是共享的(例如,在循环中创建项目时)。如果您想保留旧的构造函数,请将其中一个委托给另一个。

于 2016-07-10T15:22:50.403 回答
0

我被正式教导为所有成员数据提供 getter 和 setter,并支持指向对象的原始指针而不是自动对象

不幸的是,你被教错了。

绝对没有理由偏爱原始指针而不是任何标准库结构,例如std::vector<>, std::array<>,或者如果您需要std::unique_ptr<>, std::shared_ptr<>

错误软件中最常见的罪魁祸首是(滚动您自己的)内存管理暴露了缺陷,更糟糕的是,这些通常难以调试。

于 2016-07-08T20:22:51.747 回答
0

我被正式教导为所有成员数据提供 getter 和 setter,并支持指向对象的原始指针而不是自动对象

就个人而言,我对所有数据成员都有 setter 和 getter 没有问题。拥有并且可以节省很多悲伤是一个很好的做法,特别是如果你冒险进入线程。事实上,许多 UML 工具会为您自动生成它们。你只需要知道要返回什么。在此特定示例中,不要返回指向SmallObject1 *. SmallObject1 * const改为返回。

第二部分关于

原始指针

是为了教育目的而完成的。


对于您的主要问题:构建对象存储的方式取决于更大的设计。是BigObject唯一会使用SmallObject's 的类吗?然后我会将它们完全放在BigObject私有成员中,并在那里进行所有内存管理。如果SmallObject's 在不同的对象之间共享,并且不一定属于BigObject类,那么我会做你所做的。但是,我会将引用或指向 const 的指针存储到它们,而不是在BigObject类的析构函数中删除它们——BigObject没有分配它们,因此不应该删除它们。

于 2016-07-08T20:59:32.720 回答