Как создать приватную статическую константную строку при использовании идиомы pimpl

Фон

Я изучаю, как реализовать идиому pimpl, используя новый метод c ++ 11, описанный Хербом Саттером на этой странице: https://herbsutter.com/gotw/_100/

Я пытаюсь изменить этот пример, добавив переменную-член в частную реализацию, в частности std :: string (хотя char * имеет ту же проблему).

Проблема

Это кажется невозможным из-за использования нецелого типа static const. Инициализация внутри класса может выполняться только для целых типов, но, поскольку она статична, ее также нельзя инициализировать в конструкторе.

Решение этой проблемы - объявить частную переменную в файле заголовка и инициализировать ее в реализации, как показано здесь: Статическая постоянная строка C ++ (член класса)

Однако это решение не работает для меня, потому что оно нарушает инкапсуляцию, которую я пытаюсь достичь с помощью идиомы pimpl.

Вопрос

Как я могу скрыть нецелую статическую переменную const внутри скрытого внутреннего класса при использовании идиомы pimpl?

Пример

Вот простейший (неверный) пример, который я мог придумать, демонстрирующий проблему:

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.

Наблюдения за решением:

В моем примере выше я пытался присвоить значение TEST в объявлении класса Impl, которое находится внутри Widget.cpp, а не в его собственном файле заголовка. Определение Impl также содержится в Widget.cpp, и я считаю, что это было источником моего замешательства.

Просто переместив назначение TEST за пределы объявления Impl (но все еще в пределах определения Widget / Impl), проблема, похоже, решена.

В обоих приведенных ниже примерах решений TEST можно получить из Widget с помощью

pimpl->TEST

Попытка назначить другую строку в ТЕСТ, т.е.

pimpl->TEST = "changed"

приводит к ошибке компилятора (как и должно быть). Кроме того, попытка доступа к pimpl-> TEST извне Widget также приводит к ошибке компилятора, поскольку 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 ++ не позволила мне понять это изначально. Я надеюсь, что это позволит избежать путаницы по двум темам.


person hexsorcerer    schedule 05.08.2017    source источник
comment
Просто интересно: как это связано с Pimpl? мне кажется, нет никакой разницы от простого Как определить статическую константную строку   -  person Adrian Shum    schedule 08.08.2017
comment
Вы правы, я понял это во время обучения, но не упомянул об этом в решении, я добавил примечание к исходному сообщению для пояснения. Спасибо, что указали на это.   -  person hexsorcerer    schedule 09.08.2017
comment
Возможный дубликат Попытка определить статическую постоянную переменную в класс   -  person Adrian Shum    schedule 09.08.2017


Ответы (2)


#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&. Это позволит вам определить его в строке.

person Richard Hodges    schedule 05.08.2017
comment
Это решение действительно работает, но после написания некоторого тестового кода, чтобы опробовать его, его очень неудобно использовать на практике, а также требуется вызов функции для каждой частной строки, что говорит о том, что оно не будет хорошо масштабироваться. Считается ли это лучшим подходом к решению проблемы? - person hexsorcerer; 06.08.2017
comment
Вы должны понять, чего хотите. Если вы объявляете строку в объекте pimpl, это, вероятно, связано с тем, что вам в любом случае не нужен публичный доступ к ним. В противном случае вы бы объявили их в основном классе. - person Phil1970; 06.08.2017
comment
@ richard-hodges: Мне очень жаль, что я неправильно прочитал ваш исходный ответ, мой предыдущий комментарий касался предложения сделать ТЕСТ статической функцией; Я внес изменение, показанное в вашем примере кода, и оно работает так, как я ожидал, с добавлением еще одной const. - person hexsorcerer; 07.08.2017
comment
@hexsorcerer вообще никаких проблем - person Richard Hodges; 07.08.2017

Вы также можете заменить 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";
};

В зависимости от модели использования это может быть подходящим или нет. Если нет, то определите переменную, как описано в другом ответе.

person Phil1970    schedule 05.08.2017
comment
ошибка: переменная constexpr не может иметь нелитеральный тип 'const std :: string'. - person Jarod42; 06.08.2017
comment
Версия этого подхода std :: string дала мне аналогичную ошибку в отношении нелитерального типа. Подход char * тоже не работает, с ошибкой ISO C++ forbids converting a string constant to 'char*' - person hexsorcerer; 06.08.2017
comment
Код, который я написал, компилируется без ошибок, поэтому я не понимаю, о чем вы говорите. В моем коде нет преобразования из std::string в char *. И легко построить строку C ++ из строки C. - person Phil1970; 06.08.2017
comment
Ошибка компилятора указывает на первую двойную кавычку, окружающую тест, поэтому, похоже, речь идет о литеральной строке, присваиваемой переменной char *. - person hexsorcerer; 07.08.2017