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

Posted

技术标签:

【中文标题】避免通过操作从私有构造函数间接实例化【英文标题】:Avoid indirect instantiation from private constructor through operation 【发布时间】:2019-01-24 15:30:46 【问题描述】:

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

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

问题在于“自动”的使用。如果一个新变量声明如下:

auto newObj = obj + obj;

编译器推导出operator+的返回类型,直接将其结果赋值给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

【问题讨论】:

AFAICT 这与auto 无关,但Sample(Sample&amp;&amp;) = delete; 可能会解决您的问题? (它将阻止从临时对象创建Sample 对象) @Borgleader 你是正确的auto 不是罪魁祸首(只是写Sample&lt;double&gt; 有同样的问题)。但是,删除移动构造函数并没有帮助在 C++17 中,因为两个版本仍然可以编译(有些东西复制省略?),而在 C++14 及更低版本中删除它并没有帮助,因为那时发布的operator+ 无法编译(使用移动构造)。在后一种情况下,将移动构造函数设为私有可以解决它。 André,您需要在哪个 C++ 版本中工作? 我建议,使用 不同的 类型作为您的操作的返回类型。对于您的原始类,定义一个采用这种类型的赋值运算符,但不要定义构造函数。如果您需要代码,请告诉我。 由于 C++17 需要返回值优化,因此 AFAIK 无法阻止 T x = temporary(); 编译。 【参考方案1】:

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

您在这里混合了不同的概念。本质上,您有NamedValueSample 的概念,但您试图使每个表达式NamedValue 上的算术构成,同时也是NamedValue。那是行不通的——表达式(根据你的语义)没有名字,所以它不应该是NamedValue。因此,拥有NamedValue operator+(const NamedValue&amp; other) 没有意义。

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

但是,请注意,由于 a + b 必须 有一个类型,因此您不能阻止 auto g = a + b 编译,即使它显然是不正确的代码。询问Eigen,或任何其他表达式模板库。无论您如何选择operator+ 的返回类型,这仍然适用。所以很遗憾,你的这个愿望无法实现。

不过,我建议您不要使用普通的 T 作为返回类型,而是使用另一个类,例如 Unnamed&lt;T&gt;

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,见上文)而不是仅在转换回命名值时。

Demo here.

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

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

【讨论】:

【参考方案2】:

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

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

这需要更多的编码,因为您可能还想在这种类型上定义“操作”,以便链接工作。

【讨论】:

【参考方案3】:

一个快速的解决方法是不要从operator + 返回一个临时的Sample&lt;T&gt;。由于您只想要值部分,因此您可以直接返回它。这将代码更改为

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;

无论T 是什么,g 都会生成 g.show(); 不会编译为 g 不是 Sample&lt;T&gt;

Sample<double> g = a+c;

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


这需要添加

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

如果您希望能够像这样链接添加

a + a + a;

【讨论】:

这似乎是一个很好的解决方案,但它依赖于调用其他成员函数 (g.show()) 来给出编译器错误,它很可能会导致难以调试的错误。 @AndréLucasChinazzo 至少 gcc 的错误很容易解读 error: request for member 'show' in 'g', which is non-class type 'double' 所以您至少知道编译器将 g 视为您没有预料到的事情,并且可以追踪它。 是的。我想在大多数情况下它就足够了。如果没有调用其他成员函数(因此没有编译器错误),则总体结果应该与正确的类型相同。如果您对所编码的内容有任何了解,那么追踪错误应该很容易。

以上是关于避免通过操作从私有构造函数间接实例化的主要内容,如果未能解决你的问题,请参考以下文章

如何通过定义派生类的构造函数来实例化两个基类的私有数据成员?

《Effective Java 中文版 第2版》学习笔记 第4条:通过私有构造器强化不可实例化的能力

java中私有构造函数的作用

为啥我们需要私有构造函数?

如何访问类的私有构造函数?

谈谈JS构造函数