不变性的程度。
回复
”我目前的目标是从文件中读取数据来构造一个对象,这样以后就不能修改了
有一定程度的不变性,例如:
完全不可变。
这是旧的const,无论是对于类型还是对于单个数据成员。缺点:不能移动,例如,当用作函数返回值时会强制复制。但是,编译器可能会优化掉此类复制,并且通常会这样做。
不可变但可移动。
即使编译器没有优化,这也允许有效的函数返回值。也非常适合将原始临时文件传递到底部函数存储副本的按值调用链:它可以一直移动。
不可变但可移动且可复制分配。
Assignable 听起来可能与 immutable 的设计方向不同,实际上新手可能会认为这些属性直接冲突! Python 和 Java 字符串就是这样的例子:它们是不可变的,但可以赋值。本质上,这是封装句柄值方法的一种巧妙方法。用户代码处理句柄,但它似乎直接处理值,如果用户代码可以更改一个值,那么持有相同值句柄的用户代码的其他部分将看到变化,这与全局变量的意外变化一样不好。因此 values 需要是不可变的,但用户代码 objects 不需要(可以只是句柄)。
最后一点表明,逻辑设计级别需要将内部值与用户代码对象区分开来。
从这个观点来看,上面的第一点是关于值和对象都是不可变的;第二点具有不可变的值和通常不可变的对象,但允许从临时对象中高效且令人愉快的低级别窃取值,使它们在逻辑上为空;第三点具有不可变的值,但对象在复制分配和从临时对象移动方面都是可变的。
数据。
对于所有这三种可能性,我们可以定义一个简单的内部 Data 类,如下所示:
数据.hpp:
#pragma once
#include "cppx.hpp" // cppx::String, an alias for std::wstring
namespace my {
using cppx::String;
struct Data
{
String name;
int birthyear;
};
} // namespace my
这里的 cppx.hpp 是一个小帮助文件,具有 ¹一般的便利功能,我在最后列出。
在您的实际用例中,数据类可能会有其他数据字段,但主要思想是它是一个简单的聚合类,只是数据。您可以将其视为对应于处理值方法中的“值”。接下来,让我们定义一个类作为用户代码变量的类型。
完全不可变的对象。
以下类实现了用户代码对象完全不可变的思想:初始化时设置的值根本无法更改,并且一直持续到对象销毁。
Person.all_const.hpp:
#include "Data.hpp" // my::(String, Data), cppx::*
namespace my {
using cppx::int_from;
using cppx::line_from;
using cppx::In_stream; // alias std::wistream
class Person
{
private:
Data const data_;
public:
auto operator->() const noexcept
-> Data const*
{ return &data_; }
explicit Person( In_stream& stream )
try
: data_{ line_from( stream ), int_from( stream ) }
{} CPPX_RETHROW_X
};
} // namespace my
这里
data_ 成员的const 提供所需的完全不变性;
operator-> 可以轻松访问Data 字段;
operator-> 上的 noexcept 可能会在某些方面帮助编译器,但主要是为了程序员的利益,即记录此访问器不会抛出;
构造函数是explicit,因为在设计级别它不提供来自流参数的转换;
对line_from 和int_from 的调用顺序以及流中行的使用顺序得到保证“因为这是花括号初始化列表;
line_from 和int_from 函数是<cppx.hpp> 帮助器,它们分别从指定流中读取一行并尝试分别返回完整的行字符串,以及std::stoi 生成的int,失败时抛出异常;和
-
CPPX_RETHROW_X 宏获取函数名称并追溯异常,并将该名称附加到异常消息中,作为在异常中获取简单调用堆栈跟踪的原始显式方式。
可以定义一个名为data 的访问器方法来代替operator->,例如,返回一个Data const&,但operator-> 提供了一个非常好的使用语法,如下所示:
一个示例主程序。
main.cpp:
#include PERSON_HPP // E.g. "Person.all_const.hpp"
#include <iostream>
using namespace std;
auto person_from( cppx::In_stream& stream )
-> my::Person
{ return my::Person{ stream }; }
void cppmain()
{
auto x = person_from( wcin ); // Will not be moved with the const version.
wcout << x->name << " (born " << x->birthyear << ").\n";
// Note: due to the small buffer optimization a short string may not be moved,
// but instead just copied, even if the machinery for moving is there.
auto const x_ptr = x->name.data();
auto y = move( x );
bool const was_moved = (y->name.data() == x_ptr);
wcout << "An instance was " << (was_moved? "" : "not ") << "moved.\n";
}
auto main() -> int { return cppx::mainfunc( cppmain ); }
这里的cppx::mainfunc 也是来自<cppx.hpp> 的助手,负责捕获异常并将其消息显示在std::wcerr 流上。
我使用宽流,因为这是支持 Windows 控制台程序的国际字符的最简单方法,而且它们也可以在 Unix 领域工作(至少当其中包括对 setlocale 的调用时,这也是由 cppx::mainfunc 完成的),因此它们实际上是最便携的选项:它们使这个示例最便携。 :)
最后的代码对于完全不可变的const 版本没有多大意义,所以让我们看一下可移动版本:
不可变但可移动的对象。
Person.movable.hpp
#include "Data.hpp" // my::(String, Data), cppx::*
#include <utility> // std::move
namespace my {
using cppx::In_stream;
using cppx::int_from;
using cppx::line_from;
using std::move;
class Person
{
private:
Data data_;
auto operator=( Person const& ) = delete;
auto operator=( Person&& ) = delete;
public:
auto operator->() const noexcept
-> Data const*
{ return &data_; }
explicit Person( In_stream& stream )
try
: data_{ line_from( stream ), int_from( stream ) }
{} CPPX_RETHROW_X
Person( Person&& other ) noexcept
: data_{ move( other.data_ ) }
{}
};
} // namespace my
请注意,需要明确指定移动构造函数,如上图末尾所示。
正如 g++ 解释的那样,如果不这样做,那么
” my::Person::Person(const my::Person&)' is implicitly declared as deleted because 'my::Person' declares a move constructor or move assignment operator
不可变但可移动和可分配的对象(一个漏洞!)。
要使对象可分配,只需删除= delete 声明即可。
但是这样自动移动构造函数不会被隐式删除,所以它的显式版本可以被删除,产生
Person.assignable.hpp:
#pragma once
#include "Data.hpp" // my::(String, Data), cppx::*
#include <utility> // std::move
namespace my {
using cppx::In_stream;
using cppx::int_from;
using cppx::line_from;
class Person
{
private:
Data data_;
public:
auto operator->() const noexcept
-> Data const*
{ return &data_; }
explicit Person( In_stream& stream )
try
: data_{ line_from( stream ), int_from( stream ) }
{} CPPX_RETHROW_X
};
} // namespace my
这样更短更简单,很好。
但是,由于它支持复制分配,它允许修改实例x 的部分值。
怎么样?好吧,一种方法是从x 中复制完整的Data 值,修改Data 实例,用两行上的值格式化相应的字符串,使用它来初始化std::wistringstream,将该流传递给Person 构造函数,并将该实例分配回x。呸!多么迂回的黑客!但它表明,在理论上,它是可能的,而且效率很低,例如编写用于复制可分配Person 类的set_birthyear 函数。而这样的漏洞,类型中的安全漏洞,有时会产生问题。
不过,我只是为了完整性而提及该漏洞,以便人们能够意识到它——并且可能会意识到其他代码中的类似功能漏洞。而且我认为我个人会选择这个版本的Person 类。因为越简单,就越容易使用和维护。
为了完整性:上面使用的 cppx 支持。
cppx.hpp
#pragma once
#include <iostream> // std::(wcerr, wistream)
#include <locale.h> // setlocale, LC_ALL
#include <stdexcept> // std::runtime_error
#include <string> // std::(wstring, stoi)
#include <stdlib.h> // EXIT_...
#ifndef CPPX_QUALIFIED_FUNCNAME
# if defined( _MSC_VER )
# define CPPX_QUALIFIED_FUNCNAME __FUNCTION__
# elif defined( __GNUC__ )
# define CPPX_QUALIFIED_FUNCNAME __PRETTY_FUNCTION__ // Includes signature.
# else
# define CPPX_QUALIFIED_FUNCNAME __func__ // Unqualified but portable C++11.
# endif
#endif
// Poor man's version, roughly O(n^2) in the number of stack frames unwinded.
#define CPPX_RETHROW_X \
catch( std::exception const& x ) \
{ \
cppx::fail( \
cppx::Byte_string() + CPPX_QUALIFIED_FUNCNAME + " | " + x.what() \
); \
}
namespace cppx {
using std::endl;
using std::exception;
using std::runtime_error;
using std::stoi;
using String = std::wstring;
using Byte_string = std::string;
using In_stream = std::wistream;
using Out_stream = std::wostream;
struct Sys
{
In_stream& in = std::wcin;
Out_stream& out = std::wcout;
Out_stream& err = std::wcerr;
};
Sys const sys = {};
[[noreturn]]
inline auto fail( Byte_string const& s )
-> bool
{ throw runtime_error( s ); }
inline auto line_from( In_stream& stream )
-> String
try
{
String result;
getline( stream, result ) || fail( "getline" );
return result;
} CPPX_RETHROW_X
inline auto int_from( In_stream& stream )
-> int
try
{
return stoi( line_from( stream ) );
} CPPX_RETHROW_X
inline auto mainfunc( void (&f)() )
-> int
{
setlocale( LC_ALL, "" ); // E.g. for Unixland wide streams.
try
{
f();
return EXIT_SUCCESS;
}
catch( exception const& x )
{
sys.err << "! " << x.what() << endl;
}
return EXIT_FAILURE;
}
} // namespace cppx
¹ 我认为如果 Stack Overflow C++ 社区可以对这样的文件进行标准化,以减少阅读答案示例的认知负担,也可能是问题!但我认为大多数读者会发现 my(以及其他任何人的)助手乍一看非常陌生,其次,我懒得把这个想法带到 C++ Lounge 并在那里讨论,IMO 将是这样做的方式。
² 见(Order of evaluation of elements in list-initialization)。