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

2

6

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.

Sean answered 5/8, 2017 at 18:57 Comment(3)
Just wonder: how is this related to Pimpl? it seems to me there is no difference from simply "How to define static const string"Basanite
You are correct, I figured this out during the learning experience but failed to mention it in the solution, I have added a note to the original post for clarification. Thank you for pointing this out.Sean
Possible duplicate of Trying to define a static constant variable in a classBasanite
I
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() { }

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

Incensory answered 5/8, 2017 at 19:16 Comment(4)
This solution does appear to work, but after writing some test code to try it out, it feels very awkward to use in practice, and also requires a function call for every private string, suggesting that it wouldn't scale well. Is this considered a "best practice" approach to the problem?Sean
You have to figure out what you want. If you declare the string in the pimpl object, it is probably because you don't need public access to them anyway. Otherwise, you would declare them in the main class.Slumberous
@richard-hodges: I'm sorry I misread your original reply, my previous comment was in reference to the suggestion of making TEST a static function; I made the change shown in your code example and it is working just as I expected, with the addition of another const.Sean
@hexsorcerer no problem at allIncensory
S
1

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.

Slumberous answered 5/8, 2017 at 21:10 Comment(4)
error: constexpr variable cannot have non-literal type 'const std::string'.Ventral
The std::string version of this approach gave me a similar error about a non-literal type. The char* approach is also not working, with the error ISO C++ forbids converting a string constant to 'char*'Sean
The code I wrote compile without error so I don't know what you are talking about. There is no conversion from std::string to char * in my code. And it is easy to construct a C++ string from a C one.Slumberous
The compiler error is pointing to the first double quote surrounding "test", so it seems to be talking about the literal string being assigned into a char* variable.Sean

© 2022 - 2024 — McMap. All rights reserved.