PIMPL 类 的 setter 应该是 const 成员函数吗?
Should setters of PIMPL classes be const member functions?
我经常使用 "pointer to private implementation" 类。那些 类 的 setter 方法在技术上可以是 const 成员函数,例如:
class MyPrivateClass
{
public:
int something = 1;
};
class MyClass
{
public:
// TODO: initialize pointer in constructor
// TODO: delete pointer in destructor
// Note how this setter is const!
void setSomething(int something) const
{
p->something = something;
}
private:
MyPrivateClass* p;
};
int main()
{
return 0;
}
这是可能的,因为编译器只强制执行按位常量,而不是逻辑常量,所以上面的代码应该编译得很好。
我认为那些 setter 方法应该 NOT 是 const 成员函数,让调用者知道对象实际上正在被修改(逻辑修改,而不是按位修改,由于指向实现的指针)。
我的问题是:
是否有充分的理由使这些 setter 方法成为常量成员函数?
Effective C++ 建议(在第 3 项中)尽可能始终使用 const,但我认为这不适用于我的示例。
通常,您想在接口 class 中模仿实现 class 的限定符。
如果你想花点时间,你在 C++11 中并且你使用了很多 pimpl 惯用语,那么你可以确保你使用了正确的限定符,你还可以获得对实现的适当限定引用class 做类似的事情:
#include <type_traits>
struct AImpl
{
void f1();
void f2() const;
};
template<typename T>
struct is_const;
template<typename R, typename T, typename... Args>
struct is_const<R (T::*)(Args...) const> : std::true_type {};
template<typename R, typename T, typename... Args>
struct is_const<R (T::*)(Args...)> : std::false_type {};
class A
{
AImpl * p;
template<class T>
typename std::enable_if<!is_const<T>::value, AImpl &>::type get()
{
return *p;
}
template<class T>
typename std::enable_if<is_const<T>::value, const AImpl &>::type get() const
{
return *p;
}
public:
#define GET(x) \
static_assert( \
is_const<decltype(&A::x)>::value == \
is_const<decltype(&AImpl::x)>::value, \
"Interface does not mimic the implementation" \
); \
get<decltype(&AImpl::x)>()
void f1() { GET(f1).f1(); } // OK
void f1() const { GET(f1).f1(); } // Error
void f2() { GET(f2).f2(); } // Error
void f2() const { GET(f2).f2(); } // OK
#undef GET
};
get<T>()
returns 如果成员函数指针 T 是 const,则对实现的 const 引用;否则,一个 non-const 一个。使用它已经涵盖了一个错误案例,并为您提供了适当合格的参考。
如果您想更进一步,GET()
还会检查接口的常量性是否与实现相同,涵盖其他情况。
"Use const
whenever possible" 过于简单化。这是一个很好的起点,但不是绝对规则。
在 pimpl 习语中,正如您所指出的,应用该规则为我们提供了 const
个设置器。但是一个强烈的反对意见是这破坏了封装。您的界面现在反映了一个实现选择。想象一下,您重构后不使用 pimpl。 class 的用户不应该关心这个完全内部的决定,但现在他们关心了,因为你必须从 setter 中删除 const
。
只要存在私有(对用户)但远程(来自 class)状态,任何时候都可以提出相同的论点。重构以将该状态带入 class 逻辑上需要 non-const 成员函数不被标记 const
.
如果您能想象一个合理的实现选择,要求成员函数不是 const
,那么不将其标记为 const
.
是合理的
library fundamentals TS中有class模板propagate_const
,手写起来很方便,可以帮助你const
-pimpl idiom的正确性:
#include <experimental/propagate_const>
#include <memory>
template<class T>
using pimpl = std::experimental::propagate_const<std::unique_ptr<T>>;
struct MyPrivateClass
{
int something = 1;
};
struct MyClass
{
void setSomething(int something) const
{
// error: assignment of member 'MyPrivateClass::something' in read-only object
p->something = something;
}
pimpl<MyPrivateClass> p;
};
另请注意,在另一个答案中,代码示例无法编译:
error: decltype cannot resolve address of overloaded function
is_const<decltype(&A::x)>::value == \
^
note: in expansion of macro 'GET'
void f1() { GET(f1).f1(); } // OK
我经常使用 "pointer to private implementation" 类。那些 类 的 setter 方法在技术上可以是 const 成员函数,例如:
class MyPrivateClass
{
public:
int something = 1;
};
class MyClass
{
public:
// TODO: initialize pointer in constructor
// TODO: delete pointer in destructor
// Note how this setter is const!
void setSomething(int something) const
{
p->something = something;
}
private:
MyPrivateClass* p;
};
int main()
{
return 0;
}
这是可能的,因为编译器只强制执行按位常量,而不是逻辑常量,所以上面的代码应该编译得很好。
我认为那些 setter 方法应该 NOT 是 const 成员函数,让调用者知道对象实际上正在被修改(逻辑修改,而不是按位修改,由于指向实现的指针)。
我的问题是:
是否有充分的理由使这些 setter 方法成为常量成员函数?
Effective C++ 建议(在第 3 项中)尽可能始终使用 const,但我认为这不适用于我的示例。
通常,您想在接口 class 中模仿实现 class 的限定符。
如果你想花点时间,你在 C++11 中并且你使用了很多 pimpl 惯用语,那么你可以确保你使用了正确的限定符,你还可以获得对实现的适当限定引用class 做类似的事情:
#include <type_traits>
struct AImpl
{
void f1();
void f2() const;
};
template<typename T>
struct is_const;
template<typename R, typename T, typename... Args>
struct is_const<R (T::*)(Args...) const> : std::true_type {};
template<typename R, typename T, typename... Args>
struct is_const<R (T::*)(Args...)> : std::false_type {};
class A
{
AImpl * p;
template<class T>
typename std::enable_if<!is_const<T>::value, AImpl &>::type get()
{
return *p;
}
template<class T>
typename std::enable_if<is_const<T>::value, const AImpl &>::type get() const
{
return *p;
}
public:
#define GET(x) \
static_assert( \
is_const<decltype(&A::x)>::value == \
is_const<decltype(&AImpl::x)>::value, \
"Interface does not mimic the implementation" \
); \
get<decltype(&AImpl::x)>()
void f1() { GET(f1).f1(); } // OK
void f1() const { GET(f1).f1(); } // Error
void f2() { GET(f2).f2(); } // Error
void f2() const { GET(f2).f2(); } // OK
#undef GET
};
get<T>()
returns 如果成员函数指针 T 是 const,则对实现的 const 引用;否则,一个 non-const 一个。使用它已经涵盖了一个错误案例,并为您提供了适当合格的参考。
如果您想更进一步,GET()
还会检查接口的常量性是否与实现相同,涵盖其他情况。
"Use const
whenever possible" 过于简单化。这是一个很好的起点,但不是绝对规则。
在 pimpl 习语中,正如您所指出的,应用该规则为我们提供了 const
个设置器。但是一个强烈的反对意见是这破坏了封装。您的界面现在反映了一个实现选择。想象一下,您重构后不使用 pimpl。 class 的用户不应该关心这个完全内部的决定,但现在他们关心了,因为你必须从 setter 中删除 const
。
只要存在私有(对用户)但远程(来自 class)状态,任何时候都可以提出相同的论点。重构以将该状态带入 class 逻辑上需要 non-const 成员函数不被标记 const
.
如果您能想象一个合理的实现选择,要求成员函数不是 const
,那么不将其标记为 const
.
library fundamentals TS中有class模板propagate_const
,手写起来很方便,可以帮助你const
-pimpl idiom的正确性:
#include <experimental/propagate_const>
#include <memory>
template<class T>
using pimpl = std::experimental::propagate_const<std::unique_ptr<T>>;
struct MyPrivateClass
{
int something = 1;
};
struct MyClass
{
void setSomething(int something) const
{
// error: assignment of member 'MyPrivateClass::something' in read-only object
p->something = something;
}
pimpl<MyPrivateClass> p;
};
另请注意,在另一个答案中,代码示例无法编译:
error: decltype cannot resolve address of overloaded function
is_const<decltype(&A::x)>::value == \
^
note: in expansion of macro 'GET'
void f1() { GET(f1).f1(); } // OK