在使用Nifty Counter C++Idiom时,必须调用构造函数两次

In using the Nifty Counter C++ Idiom, must the constructor be called twice?

本文关键字:构造函数 调用 两次 Nifty Counter C++Idiom      更新时间:2023-10-16

当试图用Nifty Counter C++Idiom初始化静态成员时,我遇到了一些问题。你能解释一下在以下情况下如何正确使用ideom吗?

问题似乎如下:我在不同的编译单元中有两个静态对象,其中一个使用另一个(CDataFile)的静态成员。因为本例中的初始化顺序没有定义,而且在生产环境中,它的顺序是错误的,所以我尝试使用Nifty Counter习惯用法。但现在CDataFile的静态成员似乎初始化了两次(构造函数调用了两次)。第一次,构造函数在CDataFileInitializer中被调用,这很好。之后使用静态成员(mSome被填充),但随后CSomeClass的构造函数被第二次调用,并且mSome的内容被清除。

// datafile.h
class CDataFile
{
    friend class CDataFileInitializer;
protected:
    static CSomeClass mSome;
    // other code
};
static class CDataFileInitializer
{
public:
    CDataFileInitializer();
} dfinitializer;
// datafile.cpp
static int nifty_counter;
CSomeClass CDataFile::mSome; // second initialization comes from here?
CDataFileInitializer::CDataFileInitializer()
{
    if (!nifty_counter++)
    {
        printf("CDataFileInitializer Constructorn");
        CDataFile::mSome= CSomeClass(); // first initialization
    }
}

行:

CSomeClass CDataFile::mSome;

定义CSomeClass类型的变量。此变量的初始化分为两个阶段:首先,它被初始化为零,这(大约)意味着它所在的内存全部设置为0。之后,将进行动态初始化。这将导致其构造函数运行。

dfinitializer遵循类似的"零初始化然后动态初始化"模式。在其动态初始化步骤中,它在CDataFile::mSome上调用operator=,以便将新的默认构造的CSomeClass()分配给mSome

这一步骤完全没有意义,因为mSomedfinitializer的动态初始化相对于彼此是不确定的。如果dfinialiser首先被初始化,它将尝试分配给尚未创建的对象(稍后将默认构造),如果它第二次被初始化,则它将重新分配给已经创建的对象。

代替:

CSomeClass CDataFile::mSome;

您应该创建一个可以在其中构造对象的存储区域:

alignas(CSomeClass) unsigned char CDataFile::mSome[sizeof(CSomeClass)];

然后将CDataFileInitializer更改为:

CDataFileInitializer::CDataFileInitializer()
{
    if (!nifty_counter++)
    {
        printf("CDataFileInitializer Constructorn");
        new (&CDataFile::mSome) CSomeClass();
    }
}

另一种选择是使用函数静态变量:

CSomeClass& getMSome() {
    static CSomeClass mSome;
    return mSome;
}

这将以线程安全的方式延迟初始化mSome

如果在命名空间范围内定义对象,其构造函数将为在初始化期间的某个时刻由启动代码调用。如果你想要使用漂亮的反成语,你需要以某种方式抑制它,或者让它成为一个no-op。您还必须在实际初始化器。有几种方法可以实现这一点:

  • 我所见过的大多数工业实力的实现要么在汇编程序中声明对象,或者使用编译器扩展来确保构造函数没有被调用。这不是很便携,但是对于iostream这样无论如何都无法在纯C++中实现的东西,这通常是可以接受的。(事实上,这是唯一可以接受的解决方案对于iostream对象,因为它们不允许被破坏。)

  • 我通常会安排一个特殊的无操作构造函数什么也不做。从形式上讲,它不能保证有效,但在实践中,确实如此。然后定义使用漂亮的实例使用此构造函数的反习惯用法。

  • 最后,如果您控制正在构建的类,并且实例由漂亮计数器控制,如果它可以有琐碎的构造函数,您不需要处理构造函数,只需初始化初始化器中的各种成员。

这些都不是特别好的解决方案,在新代码中,我会使用singleton习语的变体。

yes-an-no:yes它被调用两次,no在两个不同的对象上被调用。

假设你有

// A.cpp
#include "datafile.h"
...

// B.cpp
#include "datafile.h"
...

由于#include,A.cpp和B.cpp都将具有dfinitializer的本地和独立副本。

datafile.cpp依次具有nifty_counter(最好用初始值0来定义它…static int nifty_counter = 0;)和CDatafile::mSome(在文件级初始化)。

CDataFileInitializer ctor所做的是为已经初始化的mSome分配临时的CSomeClass(),该CSomeClass在运行中创建和销毁。

事实上,所有这些都是做正确事情的错误实现,只是因为CDataFile是可赋值的。

如果问题只是初始化静态数据成员,那么您所要做的就是确保包含静态成员定义(注意:定义,而不是声明)的模块中的某些内容被其他模块调用,从而在模块中产生一些副作用(只是为了避免被优化)

所以。。。让我们试试更好的

//some.h
#ifndef SOME_H_INCLUDED
#define SOME_H_INCLUDED
#include<iostream>
class CSome
{
public:
    CSome() { std::cout << "CSome["<<this<<"] default created" << std::endl; }
    CSome(const CSome& s) { std::cout << "CSome["<<this<<"] created from ["<<&s<<"]" << std::endl; }
    CSome& operator=(const CSome& s) { std::cout << "CSome["<<this<<"] assigned from ["<<&s<<"]" << std::endl; return *this; }
    CSome(CSome&& s) { std::cout << "CSome["<<this<<"] created moving ["<<&s<<"]" << std::endl; }
    CSome& operator=(CSome&& s) { std::cout << "CSome["<<this<<"] assigned moving ["<<&s<<"]" << std::endl; return *this; }
    ~CSome() { std::cout << "CSome["<<this<<"] destroyed" << std::endl; }
};
#endif // SOME_H_INCLUDED


//datafile.h
#ifndef DATAFILE_H_INCLUDED
#define DATAFILE_H_INCLUDED
#include "some.h"
class CDataFile
{
public:
protected:
    static CSome mSome;
};
static class CDataFileInitializer
{
public:
    CDataFileInitializer();
    ~CDataFileInitializer();
} datafileinitializer;

#endif // DATAFILE_H_INCLUDED

//datafile.cpp
#include "datafile.h"
#include <iostream>
static int nifty_counter = 0; //the one and only
CSome CDataFile::mSome; //define and initialize
CDataFileInitializer::CDataFileInitializer()
{
    std::cout << "CDataFileInitializer["<<this<<"] creation"<< std::endl;
    if(!nifty_counter++)
    {
        std::cout << "CDataFileInitializer FIRST INITIALIZATION"<< std::endl;
    }
}
CDataFileInitializer::~CDataFileInitializer()
{
    std::cout << "CDataFileInitializer["<<this<<"] destruction"<< std::endl;
    if(!--nifty_counter)
    {
        std::cout << "CDataFileInitializer LAST DESTRUCTION"<< std::endl;
    }
}

//A.cpp
#include <iostream>
static class A
{
public:
    A() { std::cout << "initializing A.cpp" << std::endl; }
    ~A() { std::cout << "cleaning A.cpp" << std::endl; }
} a;
#include "datafile.h"
// other a.cpp code ...
void call_a() { std::cout << "do something in a.ccp" << std::endl; }

//B.cpp
#include <iostream>
static class B
{
public:
    B() { std::cout << "initializing B.cpp" << std::endl; }
    ~B() { std::cout << "cleaning B.cpp" << std::endl; }
} b;
#include "datafile.h"
// other b.cpp code ...
void call_b() { std::cout << "do something in b.ccp" << std::endl; }

//main.cpp
#include <iostream>
void call_a();
void call_b();
int main()
{
    std::cout << "main" << std::endl;
    call_a();
    call_b();
    std::cout << "main return" << std::endl;
    return 0;
}

将给出以下输出:

CDataFileInitializer[0x406035] creation
CDataFileInitializer FIRST INITIALIZATION
CSome[0x40602c] default created
initializing A.cpp
CDataFileInitializer[0x406029] creation
initializing B.cpp
CDataFileInitializer[0x406025] creation
main
do something in a.ccp
do something in b.ccp
main return
CDataFileInitializer[0x406025] destruction
cleaning B.cpp
CDataFileInitializer[0x406029] destruction
cleaning A.cpp
CSome[0x40602c] destroyed
CDataFileInitializer[0x406035] destruction
CDataFileInitializer LAST DESTRUCTION

当然,地址会根据您的机器和运行情况而变化。