组合对象初始化

时间:2016-07-08 20:11:40

标签: c++ oop object-composition

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

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

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

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

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 个答案:

答案 0 :(得分:0)

  

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

不幸的是,你被教导错了。

绝对没有理由赞成原始指针而不是@Bean @JobScope public FlatFileItemReader<?> yourReaderBean( @Value("#{jobParameters[filename]}") String filename){ FlatFileItemReader<?> itemReader = new FlatFileItemReader<?>(); itemReader.setLineMapper(lineMapper()); itemReader.setResource(new ClassPathResource(filename)); return itemReader; } std::vector<>等标准库构造,或者如果你需要std::array<>,{{1 }}

有缺陷的软件中最常见的罪魁祸首是(推出自己的)内存管理暴露了漏洞,更糟糕的是这些通常难以调试。

答案 1 :(得分:0)

  

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

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

关于

的第二部分
  

原始指针

用于教育目的。

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

答案 2 :(得分: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之前执行动态内存分配
  • 间接是访问此重要对象的成员所必需的,
  • 如果我们要支持你的默认构造函数,我们必须测试空指针
  • 破坏需要进一步的动态内存调用,

反对使用自动对象的一个​​论点是按值传递它们的 cost

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

这是GCC的-O3实现a1()

_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以来存在的所有权歧义{ {1}}(返回指向字符串副本的指针,记得自由)。

标准委员会之前有一个建议在C ++ 17中引入observer_ptr,这是一个围绕原始指针的非拥有包装。

使用这些方法引入了大量的锅炉板:

strdup()

我们知道auto so = std::make_unique<SmallObject>(1, 42.); A a(std::move(so), -1); 拥有我们分配的a实例的所有权,因为我们通过so明确授予其所有权。但所有明确的成本都会造成影响。对比:

std::move

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

所以我认为整体上很少有人喜欢用于组合的小对象的原始指针。您应该查看您的材料,从而得出结论,因为您似乎可能忽略或误解了何时使用原始指针的因素。

答案 3 :(得分:0)

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

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

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

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