Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a private static const string when using the pimpl idiom

Background

I have been learning how to implement the pimpl idiom using the newer c++11 method described by Herb Sutter at this page: https://herbsutter.com/gotw/_100/

I'm trying to modify this example by adding a member variable to the private implementation, specifically a std::string (although a char* has the same issue).

Problem

This seems to be impossible due to the use of a static const non-integral type. In-class initialization can only be done for integral types, but because it is static it can't be initialized in the constructor either.

A solution to this problem is to declare the private variable in the header file, and initialize it in the implementation, as shown here: C++ static constant string (class member)

However, this solution does not work for me because it breaks the encapsulation I'm trying to achieve through the pimpl idiom.

Question

How can I hide a non-integral static const variable within the hidden inner class when using the pimpl idiom?

Example

Here is the simplest (incorrect) example I could come up with demonstrating the problem:

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() { }

Compilation command:

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

Note that this example fails to compile because the variable TEST cannot be assigned at declaration due to it not being an integral type; however, because it is static this is required. This seems to imply that it cannot be done.

I've been searching for previous questions/answers to this all afternoon, but could not find any that propose a solution that preserves the information-hiding property of the pimpl idiom.

Solution Observations:

In my example above, I was attempting to assign the value of TEST in the Impl class declaration, which is inside of Widget.cpp rather than its own header file. The definition of Impl is also contained within Widget.cpp, and I believe this was the source of my confusion.

By simply moving the assignment of TEST outside of the Impl declaration (but still within the Widget/Impl definition), the problem appears to be solved.

In both of the example solutions below, TEST can be accessed from within Widget by using

pimpl->TEST

Attempting to assign a different string into TEST, i.e.

pimpl->TEST = "changed"

results in a compiler error (as it should). Also, attempting to access pimpl->TEST from outside of Widget also results in a compiler error because pimpl is declared private to Widget.

So now TEST is a constant string which can only be accessed by a Widget, is not named in the public header, and a single copy is shared among all instances of Widget, exactly as desired.

Solution Example (char *):

In the case of using a char *, note the addition of another const keyword; this was necessary to prevent changing TEST to point to another string literal.

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() { }

Solution Example (string):

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() { }

Update:

I realize now that the solution to this problem is completely unrelated to the pimpl idiom, and is just the standard C++ way of defining static constants. I've been used to other languages like Java where constants have to be defined the moment they are declared, so my inexperience with C++ prevented me from realizing this initially. I hope this avoids any confusion on the two topics.

like image 445
hexsorcerer Avatar asked Aug 05 '17 18:08

hexsorcerer


2 Answers

#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() { }

You might want to consider making TEST a static function which returns a const std::string&. This will allow you to defined it inline.

like image 180
Richard Hodges Avatar answered Nov 15 '22 12:11

Richard Hodges


You could also replace const by constexpr in your example and it will compile.

class Widget::Impl {
public:
    static constexpr std::string TEST = "test";  // constexpr here

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

Update:

Well, it seems that I was wrong... I always store raw string when I want constants.

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

Depending on the usage pattern, it might be appropriate or not. If not, then define the variable as explained in the other answer.

like image 1
Phil1970 Avatar answered Nov 15 '22 13:11

Phil1970