C++中的强制类型转换重载等价物

3

我想知道在C++中是否有可能实现与这个C#示例相同的强制类型转换重载:

class A {
    public static implicit operator A(string s) {
        return new A();
    }
    public static implicit operator A(double d) {
        return new A();
    }        
    static void Main(string[] args) {
        A a = "hello";
        A b = 5.0;
    }
}

在C++中,应该是这样的:
#include <string>
using namespace std;

class A{
    /*SOME CAST OVERLOADING GOES HERE*/
};


void main(){
    A a = "hello";
    A b = 5.0;
}

你能帮我解决如何进行类型转换重载吗?

我在我的回答中添加了一些细节。 - Kerrek SB
5个回答

17

通常使用构造函数来实现:

class A
{
public:
  A(const std::string & s) { /*...*/ }
  A(double d)              { /*...*/ }
  //...
};

用法:A a("hello"), b(4.2), c = 3.5, d = std::string("world");

(如果您声明构造函数为explicit,那么只允许第一种形式(使用圆括号的“直接初始化”)。否则两种形式完全相同。)

一个带有一个参数的构造函数也被称为“转换构造函数”,原因是你可以使用它从其他对象构造一个对象,即将一个double转换成一个A

隐式转换被广泛使用。例如,在以下情况下将应用隐式转换:

void f(A, const A&) { /* ... */ }

int main() { f(1.5, std::string("hello")); }

这个代码构造了临时对象 A(1.5)A("hello"),然后将它们作为参数传递给 f 函数。(正如您可以从第二个参数看到的那样,临时对象也可以绑定到常量引用。)

更新:(感谢 @cHao 查找并提供信息。)根据标准(12.3.4),“对于单个值,最多只能隐式应用一个用户定义的转换(构造函数或转换函数)。”(这指的是隐式转换;像 A a("hello"); 这样的直接初始化不属于此类*。请参见这个相关问题。)因此,遗憾的是您不能说 f("hello", "world");。(当然,您可以添加一个const char *构造函数,在新的 C++11 中,您甚至可以很轻松地将其转发到字符串构造函数。)

*) 我认为这实际上更加微妙一些。该规则只影响用户定义的转换。首先,您可以免费获得从 char (&)[6]const char * 的基本转换。然后,您可以从 const char * 获得一个隐式转换到 std::string。最后,您有一个显式的从 std::stringA 的转换构造函数。


我更倾向于说:“在C++中,你会使用构造函数来实现这个”。 在他所提供的C#代码中,它们是隐式转换运算符,不被称为构造函数。 - Ben Voigt
3
请注意,C++ 构造函数默认情况下是隐式的 - 这意味着编译器可以将其用于自动类型转换。如果要禁止这种情况,请在单参数构造函数前加上 explicit 关键字。 - Useless
1
哇塞...!这比在C#中好多了:S我从没注意到我可以像这样实例化一个对象=) - Van Coding
1
@KerrekSB:我认为 d="blah" 的形式不起作用:注意:候选构造函数不可行:从 'const char [6]' 到 'const std::string&' 没有已知的转换,根据 clang++。 - Mat
@VanCoding:我会抵制对那个声明发表评论的诱惑,但请随时在聊天室中再次提出它 :-) - Kerrek SB
显示剩余7条评论

2

转换运算符将为您的类型提供到其他类型的隐式转换。例如:

class A
{
public:
  operator std::string () const { return "foo"; }
};

然而,隐式转换可能会产生危险,因为它可能在你意料之外被使用,而你更希望出现编译错误。因此,通常最好在目标对象上实现一个转换构造函数,而不是在源对象上实现转换运算符。
class A
{ 
public:
  A(const std::string& rhs) { /* ... */ }
};

然而,这仍然可以进行一些隐式转换。考虑以下情况:

string f = "foo";
A foo = f;

f被隐式转换为A,因为转换构造函数可用。然而,最好将其结果作为编译器错误。通过标记转换构造函数为explicit,您可以轻松地从一种类型转换为另一种类型,但仅在您真正打算这样做时。

#include <string>

using namespace std;

class A
{
public:
    A() {};
    explicit A(const std::string& ) {};
};

int main()
{
    string f = "foof";
    A a1 = f;   // WONT COMPILE
    A a(f);     // WILL COMPILE
}

1

没有必要进行强制类型转换...这可以通过为您的类创建以正确参数类型为参数的构造函数来轻松完成,并使用单个参数,因此它们可以被编译器隐式调用。

如果您的构造函数需要多个非默认参数,则无法在转换操作中使用它们。

此外,如果您想要避免基于推断的类型转换对构造函数选择造成歧义,可以始终在您的构造函数中使用explicit关键字。例如,想象一种情况:

 struct test
 {
     int a;

     test (signed int b): a(b) {}
     test (unsigned int b): a(b) {}
 };

 int main()
 {
     test A = 5.0;

     return 0;
 }

这将导致编译器错误,因为在将 double 类型转换为标量类型时存在歧义... double 类型可以隐式地转换为两种类型。通过使用 explicit 关键字来解决此问题,可以执行以下操作:

struct test
{
    int a;

    test (signed int b): a(b) {}
    explicit test (unsigned int b): a(b) {}
};

现在,只有当传递给test构造函数的参数是unsigned int时,才会调用unsigned int版本。从double类型进行的转换操作将使用默认的int版本,消除了歧义问题。


1
在C#中,语义不是关于重载operator =(这是不允许的),而是提供从任意类型到类型A的转换运算符,或者从类型A到任意类型的转换运算符。
这意味着以下代码(加上A到类型转换):
class A {
    public static implicit operator A(string s) {
        return new A();
    }
    public static implicit operator A(double d) {
        return new A();
    }        
    public static implicit operator string(A a) {
        return string.Empty;
    }
    public static implicit operator double(A a) {
        return 0.0;
    }        
    static void Main(string[] args) {
        A a = "hello";    // line A
        A b = 5.0;        // line B
        a = "World";      // line C
        a = 3.14;         // line D
        double d = a ;    // line E
        string s = a ;    // line F
    }
}

适用于赋值操作,无论是简单的赋值(如C和D行)还是在声明中进行的赋值(如A和B行)。E和F行演示了如何将用户类型转换为其他类型。

如何在C++中实现?

在C++中,A和B行是对象构造,将调用相关的构造函数。

C和D行是赋值,将调用相关的赋值运算符。

因此,提供给您的C#代码的C++代码必须为字符串和双精度浮点数提供构造函数和赋值操作,如下所示:

class A
{
    public:
        A(const std::string & s) { /*...*/ }
        A(double d)              { /*...*/ }

        A & operator = (const std::string & s) { /*...*/ ; return *this ; }
        A & operator = (double d)              { /*...*/ ; return *this ; }

        // etc.
} ;

这样,你就可以拥有

void main()
{
    A a0 = "Hello" ;   // constructor
    A a1("Hello") ;    // constructor
    A a2 = 5.0 ;       // constructor
    A a3(5.0) ;        // constructor


    a0 = "Hello World" ; // assignment
    a0 = 3.14 ;          // assignment
}

C++中的强制类型转换运算符怎么样?

C++中的强制类型转换运算符与以下C#转换运算符类似:

class A
{
    static public operator string(A a)   { /*... ; return a string */ }
    static public operator double(A a)   { /*... ; return a double */ }

        // etc.
}

在C++中,强制类型转换运算符是这样写的:
class A
{
    public:
        operator std::string()   { /*... ; return a string */ }
        operator double()        { /*... ; return a double */ }

        // etc.
} ;

void main()
{
    A a ;
    std::string s ;
    double d ;

    // etc.

    s = a ;    // cast operator
    d = a ;    // cast operator
}

为什么C++这么复杂?

在C#中,可以从类类型转换/转换运算符到任何类型,或从任何类型转换到类类型。

在C++中,对于转换,必须选择一种或多种方式,即:构造函数、赋值运算符或强制类型转换运算符,因为在C++中,您对类型和操作有精细的控制,因此必须使用该精细的API。

构造意味着对象不存在,因此构造它到某个默认值然后将其分配给另一个值没有意义(这可能会影响速度)。在C#中,引用/值类型在分配之前为null/清零,因此这不是问题。

赋值意味着对象已经存在,因此可以重用相同的内部来容纳新值。在C#中,原始引用对象被丢弃,并创建另一个对象(如果创建成本高,则需要支付该费用)

至于强制类型转换,在当前情况下用于将现有类转换为另一个您无法控制的类:您无权扩展std::string以适应您的类的构造函数,并且无法向内置类型(如double)添加构造函数。


0
您可以提供一个带有所需类型一个参数的构造函数:
class Foo {

  int bar;

public:
  Foo(const int bar) : bar(bar) {};

}

Foo foo = 5;

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