C++ PIMPL类的setter应该是const成员函数吗?
我经常使用指向私有实现类的指针。这些类的setter方法在技术上可以是const成员函数,例如: 类MyPrivateClass { 公众: int某物=1; }; 类MyClass { 公众: //TODO:在构造函数中初始化指针 //TODO:在析构函数中删除指针 //注意这个setter是如何常量的! 无效设置某事物常量 { p->某物=某物; } 私人: MyPrivateClass*p; }; int main { 返回0; } 这是可能的,因为编译器只强制按位常量,而不强制逻辑常量,所以上面的代码应该可以正常编译 我认为这些setter方法不应该是const成员函数,以让调用者知道对象实际上正在被逻辑修改,而不是按位修改,因为指针指向实现 我的问题是: 有没有充分的理由让那些setter方法成为const成员函数C++ PIMPL类的setter应该是const成员函数吗?,c++,pimpl-idiom,C++,Pimpl Idiom,我经常使用指向私有实现类的指针。这些类的setter方法在技术上可以是const成员函数,例如: 类MyPrivateClass { 公众: int某物=1; }; 类MyClass { 公众: //TODO:在构造函数中初始化指针 //TODO:在析构函数中删除指针 //注意这个setter是如何常量的! 无效设置某事物常量 { p->某物=某物; } 私人: MyPrivateClass*p; }; int main { 返回0; } 这是可能的,因为编译器只强制按位常量,而不强制逻辑常量,
有效的C++建议在第3项中尽可能使用const,但我不认为这适用于我的例子。
一般来说,你想模仿接口类中实现类的限定符。 如果您想喜欢,您在C++11中使用了很多pimpl习惯用法,那么您可以确保使用了正确的限定符,还可以获得对实现类的适当限定引用,例如:
#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
};
如果成员函数指针T是const,get返回对实现的const引用;否则,将是一个非常量。使用它已经涵盖了一个错误案例,并为您提供了一个适当的限定参考
如果您想再进一步,GET还将检查接口的常量是否与实现相同,包括另一种情况。通常,您希望在接口类中模拟实现类的限定符 如果您想喜欢,您在C++11中使用了很多pimpl习惯用法,那么您可以确保使用了正确的限定符,还可以获得对实现类的适当限定引用,例如:
#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
};
如果成员函数指针T是const,get返回对实现的const引用;否则,将是一个非常量。使用它已经涵盖了一个错误案例,并为您提供了一个适当的限定参考
如果您想进一步推动,GET还将检查接口的常量是否与实现相同,包括其他情况。尽可能使用常量过于简单。这是一个很好的起点,但不是绝对的规则
正如您所注意到的,在pimpl习惯用法中,应用规则会给我们常量设置器。但一个强有力的反驳是,这破坏了封装。您的接口现在反映了一种实现选择。假设您重构后不使用pimpl。类的用户不应该关心这个完全内部的决定,但现在他们关心了,因为您必须从setter中删除const
在任何时候,只要用户私有但远离类状态,就可以使用相同的参数。重构以使该状态进入类需要逻辑上非常量成员函数不标记为常量
如果您可以想象一个合理的实现选择,要求成员函数不是const,那么不将其标记为const是合理的
库基本知识TS中有一个类模板propagate_const,但很容易手工编写,这有助于您在pimpl习惯用法中保持const的正确性:
#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
尽可能使用const过于简单化了。这是一个很好的起点,但不是绝对的规则
正如您所注意到的,在pimpl习惯用法中,应用规则会给我们常量设置器。但一个强有力的反驳是,这破坏了封装。您的接口现在反映了一种实现选择。假设您重构后不使用pimpl。类的用户不应该关心这个完全内部的决定,但现在他们关心了,因为您必须从setter中删除const
在任何时候,只要用户私有但远离类状态,就可以使用相同的参数。重构以使该状态进入类需要逻辑上非常量成员函数不标记为常量
如果您可以想象一个合理的实现选择,要求成员函数不是const,那么不将其标记为const是合理的
库基本知识TS中有一个类模板propagate_const,但很容易手工编写,这有助于您在pimpl习惯用法中保持const的正确性:
#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
我想不出为什么在几乎所有情况下,您都希望setter是非常量的,不管它是如何实现的。你可能想
摆弄实现类的常量。对于setter来说,常量是没有意义的。如果您确实有一个const对象,那么您不希望在itI上调用setter。我想不出为什么您几乎在所有情况下都希望setter是非const的,不管它是如何实现的。您可能想摆弄实现类的常量。在我看来,设置器常量是没有意义的。如果您确实有一个const对象,那么您不希望在itThanks上调用setter。我完全同意尽可能使用const过于简单化。我只是想确保我没有遗漏任何好的理由,为什么在我的示例中使用const可能是一个好主意。谢谢。我完全同意尽可能使用const过于简单化。我只是想确保我没有遗漏任何好的理由,为什么在我的示例中使用const可能是一个好主意。