2

这篇文章可能看起来太长了,只是最后的简短问题。但我还需要描述一个我刚刚提出的设计模式。也许它很常用,但我从未见过它(或者它只是不起作用:)。

首先,这里的代码(据我所知)由于“静态初始化顺序惨败”而具有未定义的行为。问题是 Spanish::s_englishToSpanish 的初始化依赖于 English::s_numberToStr,它们都是静态初始化的并且在不同的文件中,所以这些初始化的顺序是未定义的:

文件:English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    static vector<string>* s_numberToStr;
    string m_str;

    explicit English(int number)
    {
        m_str = (*s_numberToStr)[number];
    }
};

文件:英文.cpp

#include "English.h"

vector<string>* English::s_numberToStr = new vector<string>( /*split*/
[]() -> vector<string>
{
    vector<string> numberToStr;
    numberToStr.push_back("zero");
    numberToStr.push_back("one");
    numberToStr.push_back("two");
    return numberToStr;
}());

文件:西班牙语.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

typedef map<string, string> MapType;

struct Spanish {
    static MapType* s_englishToSpanish;
    string m_str;

    explicit Spanish(const English& english)
    {
        m_str = (*s_englishToSpanish)[english.m_str];
    }
};

文件:西班牙语.cpp

#include "Spanish.h"

MapType* Spanish::s_englishToSpanish = new MapType( /*split*/
[]() -> MapType
{
    MapType englishToSpanish;
    englishToSpanish[ English(0).m_str ] = "cero";
    englishToSpanish[ English(1).m_str ] = "uno";
    englishToSpanish[ English(2).m_str ] = "dos";
    return englishToSpanish;
}());

文件:StaticFiasco.h

#include <stdio.h>
#include <tchar.h>
#include <conio.h>

#include "Spanish.h"

int _tmain(int argc, _TCHAR* argv[])
{
    _cprintf( Spanish(English(1)).m_str.c_str() ); // may print "uno" or crash

    _getch();
    return 0;
}

为了解决静态初始化顺序问题,我们使用construct-on-first-use 习惯用法,并使这些静态初始化函数本地化,如下所示:

文件:English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    string m_str;

    explicit English(int number)
    {
        static vector<string>* numberToStr = new vector<string>( /*split*/
        []() -> vector<string>
        {
            vector<string> numberToStr_;
            numberToStr_.push_back("zero");
            numberToStr_.push_back("one");
            numberToStr_.push_back("two");
            return numberToStr_;
        }());

        m_str = (*numberToStr)[number];
    }
};

文件:西班牙语.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

struct Spanish {
    string m_str;

    explicit Spanish(const English& english)
    {
        typedef map<string, string> MapT;

        static MapT* englishToSpanish = new MapT( /*split*/
        []() -> MapT
        {
            MapT englishToSpanish_;
            englishToSpanish_[ English(0).m_str ] = "cero";
            englishToSpanish_[ English(1).m_str ] = "uno";
            englishToSpanish_[ English(2).m_str ] = "dos";
            return englishToSpanish_;
        }());

        m_str = (*englishToSpanish)[english.m_str];
    }
};

但现在我们还有另一个问题。由于函数局部静态数据,这些类都不是线程安全的。为了解决这个问题,我们为这两个类添加了一个静态成员变量和一个初始化函数。然后在这个函数内部,我们通过调用每个具有函数局部静态数据的函数来强制初始化所有函数局部静态数据。因此,实际上我们在程序开始时初始化了所有内容,但仍控制初始化的顺序。所以现在我们的类应该是线程安全的:

文件:English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    static bool s_areStaticsInitialized;
    string m_str;

    explicit English(int number)
    {
        static vector<string>* numberToStr = new vector<string>( /*split*/
        []() -> vector<string>
        {
            vector<string> numberToStr_;
            numberToStr_.push_back("zero");
            numberToStr_.push_back("one");
            numberToStr_.push_back("two");
            return numberToStr_;
        }());

        m_str = (*numberToStr)[number];
    }

    static bool initializeStatics()
    {
        // Call every member function that has local static data in it:
        English english(0); // Could the compiler ignore this line?
        return true;
    }
};
bool English::s_areStaticsInitialized = initializeStatics();

文件:西班牙语.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

struct Spanish {
    static bool s_areStaticsInitialized;
    string m_str;

    explicit Spanish(const English& english)
    {
        typedef map<string, string> MapT;

        static MapT* englishToSpanish = new MapT( /*split*/
        []() -> MapT
        {
            MapT englishToSpanish_;
            englishToSpanish_[ English(0).m_str ] = "cero";
            englishToSpanish_[ English(1).m_str ] = "uno";
            englishToSpanish_[ English(2).m_str ] = "dos";
            return englishToSpanish_;
        }());

        m_str = (*englishToSpanish)[english.m_str];
    }

    static bool initializeStatics()
    {
        // Call every member function that has local static data in it:
        Spanish spanish( English(0) ); // Could the compiler ignore this line?
        return true;
    }
};

bool Spanish::s_areStaticsInitialized = initializeStatics();

问题是:某些编译器是否有可能优化掉那些对具有本地静态数据的函数(在这种情况下为构造函数)的调用?所以问题是究竟什么是“有副作用”,据我了解,这意味着编译器不允许对其进行优化。拥有函数局部静态数据是否足以让编译器认为函数调用不能被忽略?

4

4 回答 4

1

好的,简而言之:

  1. 我不明白为什么类的静态成员需要公开——它们是实现细节。

  2. 不要将它们设为私有,而是将它们设为编译单元的成员(实现您的类的代码所在的位置)。

  3. 用于boost::call_once执行静态初始化。

首次使用时的初始化相对容易强制执行排序,而破坏则更难按顺序执行。但是请注意,在 call_once 中使用的函数不得抛出异常。因此,如果它可能失败,您应该留下某种失败状态并在调用后检查它。

(我将假设在您的真实示例中,您的负载不是您硬编码的东西,而是更有可能加载某种动态表,因此您不能只创建一个内存数组)。

于 2012-01-11T15:53:15.660 回答
1

为什么不直接将 English::s_numberToStr 隐藏在公共静态函数后面并完全跳过构造函数语法?使用DCLP确保线程安全。

我强烈建议避免初始化涉及非平凡副作用的类静态变量。作为一种通用的设计模式,它们往往导致的问题多于解决的问题。无论您在这里担心什么性能问题都需要证明,因为我怀疑它们在现实世界的情况下是否可以测量。

于 2012-01-18T16:56:33.490 回答
1

C++11 标准的第 1.9 节“程序执行”[intro.execution] 说

1 本国际标准中的语义描述定义了一个参数化的非确定性抽象机。... 需要符合要求的实现来模拟(仅)抽象机的可观察行为,如下所述。
...

5 执行格式良好的程序的一致实现应产生与具有相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。
...

8 对一致性实现的最低要求是:
— 对易失性对象的访问严格按照抽象机的规则进行评估。
— 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序可能产生的结果之一相同。
— 交互式设备的输入和输出动态应以这样一种方式发生,即在程序等待输入之前实际提供提示输出。构成交互式设备的内容是实现定义的。
这些统称为程序的可观察行为
...

12 访问由 volatile glvalue (3.10) 指定的对象、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数都是副作用,它们是执行环境状态的变化.

此外,在 3.7.2“自动存储持续时间”[basic.stc.auto] 中,据说

3 如果具有自动存储持续时间的变量具有初始化或具有副作用的析构函数,则不应在其块结束之前将其销毁,即使看起来未使用也不应作为优化消除,除非是类对象或者它的复制/移动可以按照 12.8 中的规定被消除。

12.8-31 描述了复制省略,我认为这在这里无关紧要。

So the question is whether the initialization of your local variables has side effects that prevent it from being optimized away. Since it can perform initialization of a static variable with an address of a dynamic object, I think it produces sufficient side effects (e.g. modifies an object). Also you can add there an operation with a volatile object, thus introducing an observable behavior which cannot be eliminated.

于 2012-01-18T22:31:31.860 回答
0

也许你需要做额外的工作来控制初始化顺序。像,

class staticObjects
{
    private:
    vector<string>* English::s_numberToStr;
    MapType* s_englishToSpanish;
};

static staticObjects objects = new staticObjects();

然后定义一些接口来检索它。

于 2012-01-17T15:00:26.560 回答