C++ 强制类型转换会创建一个新对象吗?

10

如标题所示,我的问题是:C++类型转换是否会创建目标类的新对象。当然,在询问之前我已经使用了谷歌、MSDN、IBM和stackoverflow的搜索工具,但我找不到适当的答案。

让我们考虑以下使用虚继承解决钻石问题的实现:

#include <iostream>
#include <cstdlib>

struct A
{
  int a;

  A(): a(2) {  }
};

struct B: virtual public A
{
  int b;

  B(): b(7) {  }
};

struct C: virtual public A
{
  int c;

  C(): c(1) {  }
};

struct END: virtual public B, virtual public C
{
  int end;

  END(): end(8) {  }
};

int main()
{
  END *end = new END();
  A *a = dynamic_cast<A*>(end);
  B *b = dynamic_cast<B*>(end);
  C *c = dynamic_cast<C*>(end);

  std::cout << "Values of a:\na->a: " << a->a << "\n\n";
  std::cout << "Values of b:\nb->a: " << b->a << "\nb->b: " << b->b << "\n\n";
  std::cout << "Values of c:\nc->a: " << c->a << "\nc->c: " << c->c << "\n\n";

  std::cout << "Handle of end: " << end << "\n";
  std::cout << "Handle of a: " << a << "\n";
  std::cout << "Handle of b: " << b << "\n";
  std::cout << "Handle of c: " << c << "\n\n";
  system("PAUSE");
  return 0;
}

据我理解,B和C的实际结构通常包括A的嵌入式实例和B或C的变量,但为了避免不确定性,虚拟A被合并成END中的一个嵌入对象,这样B和C的实际结构被销毁了。由于(正如我一直认为的那样)dynamic_cast通常只是将指针存储的地址增加嵌入式(转换的)目标类的偏移量,所以由于目标(B或C)类分成多个部分,会出现问题。

但是,如果我使用MSVC++ 2011 Express运行该示例,一切都会按预期发生(即运行,所有*.a输出2),指针仅略有差异。因此,我怀疑这些转换仍然只是将源指针的地址移动B / C实例的内部偏移量。

但是,如何实现呢? B / C的结果实例如何知道共享的A对象的位置。由于END对象中只有一个A对象,而在B和C中通常有一个A对象,因此B或C必须没有A的实例,但实际上它们似乎都有A的实例。

或者虚拟只是将对A的成员的调用委托给一个中央A对象,而不删除每个从A继承虚拟的基类的相应A对象的内部结构(即,虚拟实际上是否不破坏继承和嵌入对象的内部结构,而只是不使用它们的虚拟化(=共享)成员)?

或者虚拟函数为这些转换对象创建一个新的“偏移映射”(即告诉所有成员相对于指向类实例的指针的地址偏移量的映射,我不知道实际术语)以处理它们的“分布性”?

希望我已经澄清了一切,非常感谢
BlueBlobb

附言:
如果有语法错误,请见谅,我只是一个爱喝啤酒的巴伐利亚人,不是母语使用者:P

编辑:
我添加了这些行来输出所有int a的地址:

  std::cout << "Handle of end.a: " << &end->a << "\n";
  std::cout << "Handle of a.a: " << &a->a << "\n";
  std::cout << "Handle of a.b: " << &b->a << "\n";
  std::cout << "Handle of a.c: " << &c->a << "\n\n";

它们是相同的,这意味着确实只有一个A对象。


1
但简短的回答是“不行”。http://www.parashift.com/c++-faq/multiple-inheritance.html - Adam Burry
请注意,对于从A派生的B和C,您需要使用virtual public,但是对于从B和C派生的END,则不需要。一个普通的struct END : public B, public C就足够了。 - WhozCraig
建议更改标题以匹配实际问题(似乎是:“在虚拟继承的情况下,dynamic_cast如何“知道”该做什么?”)。 - M.M
4个回答

6
我的问题很简单,就是C ++强制类型转换是否会创建目标类的新对象。
是的,强制类型转换到类类型将创建该类型的新临时对象。
请注意,您的示例没有在任何地方进行类转换:它执行的唯一转换是指针类型的转换。这些转换确实会创建指针的新实例,但不会创建所指向的对象的新实例。我不确定您的示例应该演示什么,以及它如何与您提出的问题相关。
此外,dynamic_cast在您使用它的地方是不必要的;隐式转换同样有效。
因为(正如我一直认为的那样)dynamic_cast通常只会通过嵌入(转换的)目标类的偏移量来增加指针存储的地址
你可能正在想static_cast之类的东西。dynamic_cast更加强大。例如,它可以从B *转换为C *,即使它们在编译时不相关,也可以通过进入END *然后返回另一个分支来实现。dynamic_cast利用运行时类型信息。
B / C的结果实例如何知道共享A对象的位置。
这取决于实现。典型的实现将在派生类实例中保留空间以存储到其虚基类实例的偏移量。最终派生类的构造函数初始化所有这些偏移量。

1
在我谦逊的意见中,@Mark Ransom 在这个案例中是错误的。但请再次注意,您的示例与您的问题不匹配。您正在询问对“目标类”的转换,但您的示例并没有将其转换为类类型,而是将其转换为指针类型。当然,这不会创建一个新的类实例 - 它创建一个新的指针实例。 - Igor Tandetnik
如果我理解正确的话,B *b = &((B)(*end)); 不会和 end 中的 B 对象相同。 - Seradir
这个例子无法编译:你不能获取一个右值的地址。将其改为 const B& b = ((B)(*end));。是的,这会创建一个新的 B 实例,从 *endB 子对象复制构造,并将其绑定到引用上。 - Igor Tandetnik
此时,由于代码片段密集,很难猜测您正在运行的确切代码。如果这是您的原始代码,则它从未创建任何副本,因此结果是预期的(析构函数未被调用的原因是因为您正在泄漏堆分配的 END 实例)。 - Igor Tandetnik
谢谢,从现在开始,我将使用它以兼容其他符合标准的编译器。 - Seradir
显示剩余7条评论

5
不,你只是看到了多重继承的影响。为了将指针转换为不同的基础类型,它必须被调整为表示该确切类型的对象部分。编译器知道指针的原始类型和结果类型,因此可以应用必要的偏移量。为了使派生类型满足“is-a”要求,它必须具有构建所有基础类型的仿真所需的必要结构。
有一种情况下,强制转换可以创建一个新对象,那就是当您将其转换为非指针或引用类型时。通常,除非您已为该类型定义了转换运算符,否则这将不可能。

派生类 d; static_cast<Base>(d); 在这里还会创建一个新的临时 Base 实例,不需要重载运算符。 - Igor Tandetnik
当说“必须进行调整”时,您是什么意思?根据我的编辑说明,它们都使用相同的A对象(至少是相同的a),因此仅在转换为A部分并仍然使用与所有其他A对象相同的“偏移映射”(或者无论如何称呼)时移动指针是行不通的。或者我对“必须进行调整”的部分或其他任何内容的理解是否有误? - Seradir

0

至少在VS 2017中使用MSVC,答案是“很可能”。

// Value is a struct that contains a member: std::string _string;
// _value is a std::variant<> containing a Value as one member

template <> std::string const &Get<std::string>() const
{
    // Required pre-condition: _value.index() == TYPE_VALUE
    Value const &value = std::get<TYPE_VALUE>(_value);
    return static_cast<std::string>(value._string);
}

std::string const &test()
{
    static std::string x = "hello world";
    return static_cast<std::string>(x);
}

Get()是一个非常小的代码片段,来自于一个更大的项目,并且需要数百行其他代码的支持才能运行。 test()是我快速组合起来进行调查的。

按照现有的写法,Get()会生成以下警告:

warning C4172: returning address of local variable or temporary

test()则可以编译通过。如果我从Get()中删除static_cast<>,它也可以编译通过。

P.S. 回想起来,我应该将_value重命名为类似_payload的名称,因为它可能包含的不仅仅是一个Value


这与问题中的代码无关。(也许您试图按字面意义回答标题,但我认为从阅读问题中可以清楚地看出OP选择了一个糟糕的标题) - M.M
@M.M 可能是一个糟糕的第一句话... 话虽如此,我将把这个问题留给社区决定是否应该保留,因为我在这里处理我的答案中提到的问题,而这个页面是谷歌提供的第一个链接之一。 :/ - dgnuff

0

你提供的例子使用了指针。

A* a = dynamic_cast<A*>(end);

所以,这里唯一创建的“新”东西是另一个指针,它将指向“end”指向的对象的“A” vtable。它实际上并没有构造您正在使用的类/结构类型的新对象。
与之形成对比的是:
A a;
B b(a);

这里创建了一个新对象。但是,强制转换不会创建目标转换类型的新对象。

指针之所以不同,是因为它们指向底层对象数据部分之前的不同虚函数表。

例如:

#include <iostream>

using namespace std;

struct A {
    int a[64];
    A() { cout << "A()" << endl; }
    A(const A&) { cout << "A(A&)" << endl; }
    A& operator = (const A&) { cout << "A=A" << endl; return *this; }
};

struct B : virtual public A {
    int b[64];
    B() { cout << "B()" << endl; }
    B(const B&) { cout << "B(B&)" << endl; }
    B(const A&) { cout << "B(A&)" << endl; }
    B& operator = (const B&) { cout << "B=B" << endl; return *this; }
    B& operator = (const A&) { cout << "B=A" << endl; return *this; }
};

struct C : virtual public A {
    int c[64];
    C() { cout << "C()" << endl; }
    C(const C&) { cout << "C(C&)" << endl; }
    C(const B&) { cout << "C(B&)" << endl; }
    C(const A&) { cout << "C(A&)" << endl; }
    C& operator = (const C&) { cout << "C=C" << endl; return *this; }
    C& operator = (const B&) { cout << "C=B" << endl; return *this; }
    C& operator = (const A&) { cout << "C=A" << endl; return *this; }
};

struct END : virtual public B, C {
    int end[64];
    END() { cout << "END()" << endl; }
    END(const END&) { cout << "END(END&)" << endl; }
    END(const C&) { cout << "END(C&)" << endl; }
    END(const B&) { cout << "END(B&)" << endl; }
    END(const A&) { cout << "END(A&)" << endl; }
    END& operator = (const END&) { cout << "END=END" << endl; return *this; }
    END& operator = (const C&) { cout << "END=C" << endl; return *this; }
    END& operator = (const B&) { cout << "END=B" << endl; return *this; }
    END& operator = (const A&) { cout << "END=A" << endl; return *this; }
};

int main() {
    END* end = new END();

    A *a = dynamic_cast<A*>(end);
    B *b = dynamic_cast<B*>(end);
    C *c = dynamic_cast<C*>(end);

    std::cout << "end = " << (void*)end << std::endl;
    std::cout << "a = " << (void*)a << std::endl;
    std::cout << "b = " << (void*)b << std::endl;
    std::cout << "c = " << (void*)c << std::endl;

    // the direct pointers are going to have to differ
    // to point to the correct vtable. what about 'a' in all cases?
    std::cout << "end->a = " << (void*)&(end->a) << std::endl;
    std::cout << "a->a = " << (void*)&(a->a) << std::endl;
    std::cout << "b->a = " << (void*)&(b->a) << std::endl;
    std::cout << "c->a = " << (void*)&(c->a) << std::endl;


}

你可以在这里看到它的运行结果:http://ideone.com/0QAoWE


除非目标类型是引用,否则也会发生这种情况。您自己说过,将强制转换为指针类型会创建该类型的新对象。 - Igor Tandetnik
指针是对象,就像您承认的那样——“创建了一个新对象-类型为X的新指针”。 - Igor Tandetnik
我试图幽默一下。 - kfsone
1
根据C++标准,"对象"是一种不同的东西:1.8p1。C++程序中的构造体可以创建、销毁、引用、访问和操作对象。一个"对象"是存储区域。 - Igor Tandetnik
我明白。为了不让我感到困惑,您使用了我的定义(对象等于实例),而不是标准定义(任何堆或栈区域都是对象)。实际上,在创建我的示例时,我的主要目的是创建一个情况,其中会创建新实例,但我完全失败了 ;) - Seradir
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接