C++ 在C+;中创建一个不变且高效的类的惯用方法+;
我想做这样的事情(C#) 问题1:C++ 在C+;中创建一个不变且高效的类的惯用方法+;,c++,constants,immutability,const-cast,C++,Constants,Immutability,Const Cast,我想做这样的事情(C#) 问题1: OtherImmutableObject o1(1,2); OtherImmutableObject o2(2,3); o1 = o2; // error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)` 编辑:这很重要,因为我想将不可变对象存储在std::vector中,但
OtherImmutableObject o1(1,2);
OtherImmutableObject o2(2,3);
o1 = o2; // error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)`
编辑:这很重要,因为我想将不可变对象存储在std::vector
中,但收到错误:使用删除的函数“OtherImmutableObject&OtherImmutableObject::operator=(OtherImmutableObject&&)
2。使用get方法和返回值,但这意味着必须复制大型对象,我想知道如何避免这种低效。建议使用get解决方案,但它没有说明如何在不复制原始对象的情况下处理传递的非基本对象
解决方案2:
class OtherImmutableObject {
int i1;
int i2;
public:
OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
int GetI1() { return i1; }
int GetI2() { return i2; }
}
class ImmutableObject {
int i1;
OtherImmutableObject o;
std::vector<OtherImmutableObject> v;
public:
ImmutableObject(int i1, OtherImmutableObject o,
std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
int GetI1() { return i1; }
OtherImmutableObject GetO() { return o; } // Copies a value that should be immutable and therefore able to be safely used elsewhere.
std::vector<OtherImmutableObject> GetV() { return v; } // Copies the vector.
}
问题3:
ImmutableObject immutable_object(1,o,v);
// elsewhere in code...
OtherImmutableObject& other_immutable_object = immutable_object.GetO();
// Somewhere else immutable_object goes out of scope, but not other_immutable_object
// ...and then...
other_immutable_object.GetI1();
// The previous line is undefined behaviour as immutable_object.o will have been deleted with immutable_object going out of scope
由于从任何
Get
方法返回引用,可能会发生未定义的行为。通过利用std::unique_ptr
或std::shared_ptr
基本上可以获得所需。如果您只需要这些对象中的一个,但允许将其四处移动,则可以使用std::unique\u ptr
。如果要允许多个对象(“副本”)都具有相同的值,则可以使用std::shared\u Ptr
。使用别名来缩短名称并提供一个工厂函数,它变得非常轻松。这将使您的代码看起来像:
class ImmutableClassImpl {
public:
const int i;
const OtherImmutableClass o;
const ReadOnlyCollection<OtherImmutableClass> r;
public ImmutableClassImpl(int i, OtherImmutableClass o,
ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}
using Immutable = std::unique_ptr<ImmutableClassImpl>;
template<typename... Args>
Immutable make_immutable(Args&&... args)
{
return std::make_unique<ImmutableClassImpl>(std::forward<Args>(args)...);
}
int main()
{
auto first = make_immutable(...);
// first points to a unique object now
// can be accessed like
std::cout << first->i;
auto second = make_immutable(...);
// now we have another object that is separate from first
// we can't do
// second = first;
// but we can transfer like
second = std::move(first);
// which leaves first in an empty state where you can give it a new object to point to
}
然后两个对象都指向同一个对象,但都不能修改它
struct
struct Immutable {
const std::string str;
const int i;
};
您可以实例化和复制它们,读取数据成员,但仅此而已。移动从另一个实例的右值引用构造实例时仍然复制
Immutable obj1{"...", 42};
Immutable obj2 = obj1;
Immutable obj3 = std::move(obj1); // Copies, too
obj3 = obj2; // Error, cannot assign
这样,您就可以真正确保类的每个用法都尊重不变性(假设没有人做不好的const\u cast
事情)。附加功能可以通过自由函数提供,将成员函数添加到数据成员的只读聚合中没有意义private
数据成员和getter成员函数:
class Immutable {
public:
Immutable(std::string str, int i) : str{std::move(str)}, i{i} {}
const std::string& getStr() const { return str; }
int getI() const { return i; }
private:
std::string str;
int i;
};
用法是一样的,但move构造确实会移动
Immutable obj1{"...", 42};
Immutable obj2 = obj1;
Immutable obj3 = std::move(obj1); // Ok, does move-construct members
现在,您可以控制是否允许分配任务。如果不需要,只需删除赋值运算符,否则使用编译器生成的赋值运算符或实现自己的赋值运算符
obj3 = obj2; // Ok if not manually disabled
我认为最惯用的方式是:
struct OtherImmutable {
int i1;
int i2;
OtherImmutable(int i1, int i2) : i1(i1), i2(i2) {}
};
但是。。。那不是一成不变的吗
确实如此,但您可以将其作为值传递:
void frob1() {
OtherImmutable oi;
oi = frob2(oi);
}
auto frob2(OtherImmutable oi) -> OtherImmutable {
// cannot affect frob1 oi, since it's a copy
}
更好的是,不需要局部变异的地方可以将其局部变量定义为常量:
auto frob2(OtherImmutable const oi) -> OtherImmutable {
return OtherImmutable{oi.i1 + 1, oi.i2};
}
由于C++的普适语义,C++中的不可改变性不能直接与大多数其他流行语言的不可改变性相比较。你必须弄清楚“不可变”是什么意思 您希望能够将新值分配给
OtherImmutableObject
类型的变量。这是有道理的,因为您可以使用C#中类型为ImmutableObject
的变量来实现这一点
在这种情况下,获取所需语义的最简单方法是
struct OtherImmutableObject {
int i1;
int i2;
};
这看起来可能是可变的。毕竟,你可以写作
OtherImmutableObject x{1, 2};
x.i1 = 3;
但是第二行的效果(忽略并发性…)与
x = OtherImmutableObject{3, x.i2};
因此,如果您想允许赋值给OtherImmutableObject
类型的变量,那么禁止直接赋值给成员是没有意义的,因为它不提供任何额外的语义保证;它所做的只是使同一抽象操作的代码变慢。(在这种情况下,大多数优化编译器可能会为这两个表达式生成相同的代码,但如果其中一个成员是std::string
,它们可能不够聪明,无法做到这一点。)
注意这是C++中基本上所有标准类型的行为,包括<代码> int <代码>,<代码> STD::复杂< /COD>,<代码> STD::String 等等。它们都是可变的,在这个意义上,你可以给它们分配新的值,并且在你可以做的唯一的抽象意义上是不可变的(抽象的)。改变它们就是给它们分配新的值,就像C#中的不可变引用类型一样
如果您不想要这种语义,那么您唯一的其他选择就是禁止赋值。我建议通过将变量声明为const
,而不是将该类型的所有成员声明为const
,因为它为您提供了更多使用该类的选项。例如,您可以创建该类的一个初始可变实例,在其中构建一个值,然后仅使用const
引用“冻结”该类,就像将StringBuilder
转换为string
,但无需复制它的开销
(将所有成员声明为const
的一个可能原因可能是,它在某些情况下允许更好的优化。例如,如果函数获得OtherImmutableObject const&
,并且编译器看不到调用站点,则在调用其他未知代码时缓存成员的值是不安全的,因为ing对象可能没有const
限定符。但是如果实际成员声明为const
,那么我认为缓存这些值是安全的。)C++不太能够将类预定义为不可变或常量
在某个时刻,您可能会得出结论,您不应该使用constauto frob2(OtherImmutable const oi) -> OtherImmutable {
return OtherImmutable{oi.i1 + 1, oi.i2};
}
struct OtherImmutableObject {
int i1;
int i2;
};
OtherImmutableObject x{1, 2};
x.i1 = 3;
x = OtherImmutableObject{3, x.i2};
typedef class _some_SUPER_obtuse_CLASS_NAME_PLEASE_DONT_USE_THIS { } const Immutable;
/* const-correct */ class C {
int f1_;
int f2_;
const int f3_; // Semantic constness : initialized and never changed.
};
shared_ptr<const C> ptr = make_shared<const C>(f1, f2, f3);
struct immu_tag_t {};
template<class T>
struct immu:std::shared_ptr<T const>
{
using base = std::shared_ptr<T const>;
immu():base( std::make_shared<T const>() ) {}
template<class A0, class...Args,
std::enable_if_t< !std::is_base_of< immu_tag_t, std::decay_t<A0> >{}, bool > = true,
std::enable_if_t< std::is_construtible< T const, A0&&, Args&&... >{}, bool > = true
>
immu(A0&& a0, Args&&...args):
base(
std::make_shared<T const>(
std::forward<A0>(a0), std::forward<Args>(args)...
)
)
{}
template<class A0, class...Args,
std::enable_if_t< std::is_construtible< T const, std::initializer_list<A0>, Args&&... >{}, bool > = true
>
immu(std::initializer_list<A0> a0, Args&&...args):
base(
std::make_shared<T const>(
a0, std::forward<Args>(args)...
)
)
{}
immu( immu_tag_t, std::shared_ptr<T const> ptr ):base(std::move(ptr)) {}
immu(immu&&)=default;
immu(immu const&)=default;
immu& operator=(immu&&)=default;
immu& operator=(immu const&)=default;
template<class F>
immu modify( F&& f ) const {
std::shared_ptr<T> ptr;
if (!*this) {
ptr = std::make_shared<T>();
} else {
ptr = std::make_shared<T>(**this);
}
std::forward<F>(f)(*ptr);
return {immu_tag_t{}, std::move(ptr)};
}
};
immu<int> immu_null_int{ immu_tag_t{}, {} };
immu<int> immu_int;
immu<int> immu_int = 7;
struct data;
using immu_data = immu<data>;
struct data {
int i;
other_immutable_class o;
std::vector<other_immutable_class> r;
data( int i_in, other_immutable_class o_in, std::vector<other_immutable_class> r_in ):
i(i_in), o(std::move(o_in)), r( std::move(r_in))
{}
};
immu_data a( 7, other_immutable_class{}, {} );
immu_data b = a.modify([&](auto& b){ ++b.i; b.r.emplace_back() });
class ImmutableObject {
ImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
const int i1;
const int i2;
}
ImmutableObject o1(1,2):
ImmutableObject o2(2,3);
o1 = o2; // Doesn't compile, because immutable objects are by definition not mutable.
class ImmutableObject {
ImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
const int i1;
const int i2;
}
std::shared_ptr<ImmutableObject> o1 = std::make_shared<ImmutableObject>(1,2);
std::shared_ptr<ImmutableObject> o2 = std::make_shared<ImmutableObject>(2,3);
o1 = o2; // Does compile because shared_ptr is mutable.