当使用pimpl模式时如何创建一个私有的静态常量字符串?

6

背景

我一直在学习如何使用Herb Sutter在此页面描述的较新的c++11方法实现pimpl惯用语法:https://herbsutter.com/gotw/_100/

我正在尝试通过向私有实现添加成员变量来修改此示例,具体是std::string(尽管char*也存在同样的问题)。

问题

由于使用了静态const非整数类型,因此似乎这是不可能的。只能对整数类型进行类内初始化,但由于它是静态的,因此也无法在构造函数中初始化。

解决此问题的方法是在头文件中声明私有变量,并在实现中进行初始化,如下所示:C++ static constant string (class member)

但是,这种解决方法对我没有用,因为它破坏了我尝试通过pimpl惯用语法实现的封装性。

问题

当使用pimpl惯用语法时,如何在隐藏的内部类中隐藏非整数静态const变量?

示例

以下是我能想到的最简单(不正确)的示例,演示了该问题:

Widget.h:

#ifndef WIDGET_H_
#define WIDGET_H_

#include <memory>

class Widget {
public:
    Widget();
    ~Widget();
private:
    class Impl;
    std::unique_ptr<Impl> pimpl;
};

#endif

Widget.cpp:

#include "Widget.h"
#include <string>

class Widget::Impl {
public:
    static const std::string TEST = "test";

    Impl() { };
    ~Impl() { };
};

Widget::Widget() : pimpl(new Impl()) { }
Widget::~Widget() { }

编译命令:

g++ -std=c++11 -Wall -c -o Widget.o ./Widget.cpp

注意,此示例无法编译,因为变量TEST不能在声明时赋值,因为它不是整型;但是,由于它是静态的,所以这是必需的。这似乎意味着无法做到。
我一直在搜索先前关于这个问题的问题/答案,但找不到任何保留pimpl惯用语信息隐藏属性的解决方案。
解决方案观察:
在上面的示例中,我试图在Impl类声明中(在Widget.cpp而不是其自己的头文件中)分配TEST的值。Impl的定义也包含在Widget.cpp中,我认为这是我的困惑之源。
通过简单地将TEST的赋值移动到Impl声明之外(但仍在Widget / Impl定义内),问题似乎已得到解决。
在下面的两个示例解决方案中,可以使用pimpl->TEST从Widget中访问TEST。
尝试将不同的字符串分配给TEST,即 pimpl->TEST =“changed”
会导致编译器错误(应该如此)。此外,尝试从Widget外部访问pimpl->TEST也会导致编译器错误,因为pimpl被声明为Widget的私有变量。
因此,现在TEST是一个常量字符串,只能由Widget访问,在公共头文件中未命名,并且所有Widget实例之间共享单个副本,正如所需的那样。
解决方案示例(char *):
在使用char *的情况下,请注意添加另一个const关键字;这是必要的,以防止更改TEST指向另一个字符串文字。
Widget.cpp:
#include "Widget.h"
#include <stdio.h>

class Widget::Impl {
public:
    static const char *const TEST;

    Impl() { };
    ~Impl() { };
};

const char *const (Widget::Impl::TEST) = "test";

Widget::Widget() : pimpl(new Widget::Impl()) { }
Widget::~Widget() { }

解决方案示例(字符串):

Widget.cpp:

#include "Widget.h"
#include <string>

class Widget::Impl {
public:
    static const std::string TEST;

    Impl() { };
    ~Impl() { };
};

const std::string Widget::Impl::TEST = "test";

Widget::Widget() : pimpl(new Widget::Impl()) { }
Widget::~Widget() { }

更新:

我现在意识到,解决这个问题与pimpl习惯完全无关,而只是定义静态常量的标准C ++方式。 我习惯于像Java这样的其他语言,在声明时必须定义常量,因此我的C ++经验不足最初阻碍了我意识到这一点。 我希望这避免了两个主题上的任何混淆。


只是好奇:这与Pimpl有什么关系?在我看来,它似乎与简单地“如何定义静态常量字符串”没有区别。 - Adrian Shum
你说得对,我在学习过程中已经弄清楚了这一点,但是没有在解决方案中提到,我已经在原始帖子中添加了一个注释以作澄清。谢谢你指出这一点。 - hexsorcerer
可能是重复的问题:在类中尝试定义静态常量变量 - Adrian Shum
2个回答

3
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();
private:
    class Impl;
    std::unique_ptr<Impl> pimpl;
};

/*** cpp ***/

#include <string>

class Widget::Impl {
public:
    static const std::string TEST;

    Impl() { };
    ~Impl() { };
};

const std::string Widget::Impl::TEST = "test"; 

Widget::Widget() : pimpl(new Impl()) { }
Widget::~Widget() { }

你可能需要考虑将TEST定义为静态函数,返回一个const std::string&。这样可以使其内联定义。

这个解决方案似乎可以工作,但在编写一些测试代码尝试使用它后,感觉实际使用起来非常笨拙,并且需要为每个私有字符串调用一个函数,这表明它不会很好地扩展。这是否被认为是解决问题的“最佳实践”方法? - hexsorcerer
你必须弄清楚你想要什么。如果你在pimpl对象中声明字符串,那很可能是因为你不需要公共访问它们。否则,你应该在主类中声明它们。 - Phil1970
@richard-hodges:很抱歉我误读了您的原始回复,我之前的评论是关于将TEST变为静态函数的建议;我对您代码示例中所做的更改进行了修改,并且添加了另一个const,现在它的工作效果正如我所期望的那样。 - hexsorcerer
@hexsorcerer 没问题 - Richard Hodges

1
你可以在示例中将const替换为constexpr,它也将编译。
class Widget::Impl {
public:
    static constexpr std::string TEST = "test";  // constexpr here

    Impl() { };
    ~Impl() { };
};

更新:

好吧,看来我错了……当我需要常量时,我总是存储原始字符串。

class Widget::Impl {
public:
    static constexpr char * const TEST = "test";
};

根据使用模式,可能合适也可能不合适。如果不合适,则按照其他答案中所述定义变量。


错误:constexpr变量不能具有非字面类型'const std::string'。 - Jarod42
这种方法的std::string版本给了我一个类似的错误,说它不是字面类型。char方法也不起作用,出现错误“ISO C++禁止将字符串常量转换为'char'”。 - hexsorcerer
我所写的代码能够成功编译,所以我不知道你在说什么。我的代码中没有从 std::stringchar* 的转换。而且将 C 语言字符串构造成 C++ 字符串也非常容易。 - Phil1970
编译器错误指向围绕“test”的第一个双引号,因此它似乎在谈论将字面字符串分配到char*变量中。 - hexsorcerer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接