C++ PIMPL类的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; } 这是可能的,因为编译器只强制按位常量,而不强制逻辑常量,

我经常使用指向私有实现类的指针。这些类的setter方法在技术上可以是const成员函数,例如:

类MyPrivateClass { 公众: int某物=1; }; 类MyClass { 公众: //TODO:在构造函数中初始化指针 //TODO:在析构函数中删除指针 //注意这个setter是如何常量的! 无效设置某事物常量 { p->某物=某物; } 私人: MyPrivateClass*p; }; int main { 返回0; } 这是可能的,因为编译器只强制按位常量,而不强制逻辑常量,所以上面的代码应该可以正常编译

我认为这些setter方法不应该是const成员函数,以让调用者知道对象实际上正在被逻辑修改,而不是按位修改,因为指针指向实现

我的问题是:

有没有充分的理由让那些setter方法成为const成员函数


有效的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可能是一个好主意。