避免通过操作从私有构造函数间接实例化

Avoid indirect instantiation from private constructor through operation

本文关键字:构造函数 实例化 操作      更新时间:2023-10-16

我正在尝试创建一个类,其对象必须包含其值所代表内容的简短描述("名称"(。因此,唯一的公共构造函数应将字符串作为参数。

但是,对于操作,我需要创建临时(无相关名称(对象来计算要分配给现有对象的值。为此,我实现了一个私有构造函数,不应直接或间接使用它来实例化新对象 - 这些临时对象只能通过 operator= 分配给已经存在的对象,它只复制值而不是名称和值。

问题来自使用"自动"。如果按如下方式声明新变量:

auto newObj = obj + obj;

编译器推导运算符+的返回类型,并将其结果直接分配给newObj。这会导致对象具有不相关的名称,该对象应该无法实例化。

此外,仍然可以从某些函数中推断出已经存在的对象的类型,例如:

auto newObj = obj.makeNewObjWithSameTypeButOtherName("Other name");

遵循演示问题的代码:

#include <iostream>
#include <string>
using namespace std;
template<class T>
class Sample
{
public:
Sample(const string&);
Sample<T> makeNewObj(const string&);
// Invalid constructors
Sample();
Sample(const Sample&);
void operator=(const Sample&);
void operator=(const T&);
Sample<T> operator+(const Sample&) const;
void show(void);
private:
// Private constructor used during operations
Sample(const T&);
T _value;
string _name;

};
template<class T>
Sample<T>::Sample(const string& name)
{
this->_name = name;
this->_value = 0;
}
template<class T>
Sample<T>::Sample(const T&value)
{
this->_name = "Temporary variable";
this->_value = value;
}
template<class T>
Sample<T>
Sample<T>::makeNewObj(const string& name)
{
return Sample<T>(name);
}
template<class T>
void
Sample<T>::operator=(const Sample& si)
{
this->_name = this->_name; // Make explicit: Never change the name
this->_value = si._value;
}
template<class T>
void
Sample<T>::operator=(const T& value)
{
this->_name = this->_name; // Make explicit: Never change the name
this->_value = value;
}
template<class T>
Sample<T>
Sample<T>::operator+(const Sample& si) const
{
// if any of the two values are invalid, throw some error
return Sample<T>( this->_value + si._value );
}
template<class T>
void
Sample<T>::show(void)
{
cout << _name << " = " << _value << endl;
}
int main()
{
Sample<double> a("a"), b("b");
a = 1; // Sample::operator=(const T&)
b = 2.2; // Sample::operator=(const T&)
a.show(); // Output: a = 1
b.show(); // Output: b = 2.2
auto c = a.makeNewObj("c"); // Should be possible
c = a + b; // Sample::operator+(const Sample&) and Sample::operator=(const Sample&)
c.show(); // Output: c = 3.2
//    Sample<double> d; // Compiler error as expected: undefined reference to `Sample::Sample()'
//    auto f = a; // Compiler error as expected: undefined reference to `Sample::Sample(Sample const&)'
// This is what I want to avoid - should result in compiler error
auto g = a+c; // No compiler error: uses the private constructor     Sample::Sample(const T&)
g.show(); // Output: Temporary variable = 4.2  <-- !! Object with irrelevant name
}

一个快速的解决方法是不从operator +返回临时Sample<T>。 由于您只需要值部分,因此您可以只返回它。这会将代码更改为

T operator+(const Sample&) const;
template<class T>
T
Sample<T>::operator+(const Sample& si) const
{
// if any of the two values are invalid, throw some error
return  this->_value + si._value;
}

然后

auto g = a+c;

g无论T是什么,g.show();都不会编译,因为g不是Sample<T>.

Sample<double> g = a+c;

也不会工作,因为它尝试从值构造g并且该构造函数是私有的。


这将需要添加

friend T operator+(T val, Sample<T> rhs) { return val + rhs._value; }

如果您希望能够链接添加项,例如

a + a + a;

与NathanOliver的答案有些相关,但也正交:

您在这里混合了不同的概念。你基本上有NamedValueSample的概念,但你试图使每个由算术形成的表达式NamedValue也是一个NamedValue。这是行不通的 - 表达式(通过您的语义(没有名称,因此它不应该是NamedValue.因此,拥有NamedValue operator+(const NamedValue& other)是没有意义的。

Nathan 的回答通过让添加返回T来解决这个问题。这很简单。

但是,请注意,由于a + b必须具有类型,因此无法阻止auto g = a + b进行编译,即使它是明显不正确的代码。询问特征或任何其他表达式模板库。无论您如何选择operator+的返回类型,这都是正确的。所以不幸的是,你的这个愿望无法实现。

不过,我建议您不要使用纯T作为返回类型,而是使用另一个类,例如Unnamed<T>

template<class T>
class Unnamed
{
public:
explicit Unnamed(const T& value) : _value(value) {};
Unnamed<T> operator+(const Unnamed<T>& rhs) const
{
return Unnamed<T>(_value + rhs._value);
}
friend Unnamed operator+(const Unnamed& lhs, const Sample<T>& rhs);
friend Unnamed operator+(const Sample<T>& lhs, const Unnamed& rhs);
private:
T _value;
};

这允许您检查每个操作的内容(因为(a + b) + (c + d)中的中间+不能接受NamedValues,见上文(,而不是仅在转换回命名值时进行检查。

在这里演示。

您可以通过仅允许从Unnamed临时构造Sample来稍微提高编译时安全性:https://godbolt.org/g/Lpz1m5

这一切都可以比这里勾勒的更优雅。请注意,这正朝着表达式模板的方向发展。

我的建议是更改+ operator的签名(或任何其他需要实现的操作(以返回不同的类型。

比添加接受此"不同类型"的赋值运算符,但不添加复制构造函数 - 或者,为了更好地报告错误,请添加一个deleted运算符。

这将需要更多的编码,因为您可能还想在此类型上定义"操作",以便链接有效。