在函数作用域之外运行C++代码

15

(我知道)在C++中,我可以在作用域之外声明变量,但除了初始化全局/静态变量外,我无法运行任何代码/语句。


想法

使用下面的巧妙代码(例如)进行一些std::map操作是否是一个好主意?

在这里,我使用void *fakeVar并通过Fake::initializer()进行初始化,并且在其中做任何我想要的事情!

std::map<std::string, int> myMap;

class Fake
{
public:
    static void* initializer()
    {
        myMap["test"]=222;
        // Do whatever with your global Variables

        return NULL;
    }
};

// myMap["Error"] = 111;                  => Error
// Fake::initializer();                   => Error
void *fakeVar = Fake::initializer();    //=> OK

void main()
{
    std::cout<<"Map size: " << myMap.size() << std::endl; // Show myMap has initialized correctly :)
}

2
你为什么要为那个函数创建一个类? - Dawid
@Dawid 这是我的错! - Emadpres
“这个想法好不好”听起来有点基于个人意见。此外,“什么更好”(从答案来看,实际上就是这样)是一个列表问题。 - BartoszKP
每当你考虑“棘手的代码”时,难道你不是已经回答了自己的问题吗? - ErstwhileIII
10个回答

15

解决此问题的一种方法是创建一个具有执行操作的构造函数的类,然后声明该类的虚拟变量。例如:

struct Initializer
{
    Initializer()
    {
        // Do pre-main initialization here
    }
};

Initializer initializer;

当然您可以拥有多个这样的类来进行各种初始化。每个翻译单元中的顺序被指定为自上而下,但是翻译单元之间的顺序没有指定。


更一般地说,我们可以向这个构造函数添加一些参数。 - Emadpres
@Emadpres 没问题,你可以像往常一样传递参数。 - Some programmer dude
1
@Emadpres:通常这并不有用。正如您从Joachim的示例中看到的那样,传递的参数将仅向上传递5行。为什么不直接将它们放在需要它们的地方,在Initializer :: Initializer内部?就像Initializer initializer;不能访问argc / argv或其他有用的变量一样(在此问题的上下文中)。 - MSalters
在我的情况下,我需要从不同的源文件中插入地图,并且每个地图应该添加它们特定的数据。因此,在这种情况下,我需要有参数。 - Emadpres
2
@Emadpres:这会遇到一个问题,因为地图不能是全局的,因为当其他文件中的初始化程序运行时,它可能不存在。 - MSalters
2
在这种情况下,您可以在函数内将map声明为静态变量:map<string, int>& GetMap() { static map<string, int> myMap; return myMap; }。调用GetMap的第一个初始化器将使其存在。 - Scott Langham

11

你不需要一个虚假的类……你可以使用lambda表达式进行初始化

auto myMap = []{
    std::map<int, string> m;
    m["test"] = 222;
    return m;
}();

或者,如果只是简单数据,请初始化地图:

std::map<std::string, int> myMap { { "test", 222 } };

1
谢谢您提供lambda,但在我的情况下,myMap应该由不同的类进行初始化。 - Emadpres
1
我喜欢使用lambda表达式进行const声明。 - Viktor Sehr

6

使用以下诡异的代码是一个好主意吗?例如,用于进行一些std::map操作?

不是的。

任何需要可变非局部变量的解决方案都是一个可怕的主意。


5

这是个好主意吗...?

不完全是。如果有人决定在他们的“棘手初始化”中使用你的地图,但在某些系统或其他情况下,或者在特定的重新链接之后,你的地图最终被初始化了,那该怎么办呢?如果你让他们调用一个返回地图引用的静态函数,那么它可以在第一次调用时初始化。将地图设置为该函数内部的静态局部变量,就可以防止任何意外使用而没有这种保护。


谢谢。这里提到了:http://www.parashift.com/c++-faq-lite/static-init-order-on-first-use.html 。 - Emadpres

4

根据§ 8.5.2,除了使用constexpr限定符声明的对象(参见7.1.5),定义变量时的初始化可以由涉及文本和之前声明的变量和函数的任意表达式组成,而不管变量的存储持续时间。

因此,你所做的操作完全符合C++标准。话虽如此,如果需要执行“初始化操作”,最好使用类构造函数(例如包装器)。


2
当我听到“棘手的代码”时,我立刻想到了代码异味和维护噩梦。回答你的问题,不,这不是一个好主意。虽然它是有效的C++代码,但这是一种不好的实践。对于这个问题,有其他更明确、更有意义的替代方案。具体来说,你的initializer()方法返回void* NULL是没有意义的,因为它与你程序的意图无关(即你的每行代码都应该有明确的目的),而你现在又有了另一个不必要的全局变量fakeVar,它毫无意义地指向NULL。
让我们考虑一些不那么“棘手”的替代方案:
  1. If it's extremely important that you only ever have one global instance of myMap, perhaps using the Singleton Pattern would be more fitting, and you would be able to lazily initialize the contents of myMap when they are needed. Keep in mind that the Singleton Pattern has issues of its own.

  2. Have a static method create and return the map or use a global namespace. For example, something along the lines of this:

    // global.h
    namespace Global
    {
        extern std::map<std::string, int> myMap;
    };
    
    // global.cpp
    namespace Global
    {
        std::map<std::string, int> initMap()
        {
            std::map<std::string, int> map;
            map["test"] = 222;
            return map;
        }
    
        std::map<std::string, int> myMap = initMap();
    };
    
    // main.cpp
    #include "global.h"
    
    int main()
    {
       std::cout << Global::myMap.size() << std::endl;
       return 0;
    }
    
  3. If this is a map with specialized functionality, create your own class (best option)! While this isn't a complete example, you get the idea:

    class MyMap
    {
    private:
        std::map<std::string, int> map;
    
    public:
    
        MyMap()
        {
            map["test"] = 222;
        }
    
        void put(std::string key, int value)
        {
            map[key] = value;
        }
    
        unsigned int size() const
        {
            return map.size();
        }
    
        // Overload operator[] and create any other methods you need
        // ...
    };
    
    MyMap myMap;
    
    int main()
    {
       std::cout << myMap.size() << std::endl;
       return 0;
    }
    

你的最后一个选项(将所有内容放入类中)与此问题不同,无法满足在函数范围之外操作myMap的目标。 - Emadpres

2
你所做的C++代码是完全合法的。如果它对你有效,并且其他与代码一起工作的人可以维护和理解它,那就没问题。但Joachim Pileborg的示例对我来说更清晰易懂。
使用这种方式初始化全局变量可能会出现一个问题,即在初始化过程中它们相互使用。在这种情况下,确保变量按正确顺序初始化可能会很棘手。因此,我更喜欢创建InitializeX,InitializeY等函数,并从Main函数中显式调用它们以正确的顺序。
错误的顺序也可能导致程序退出时出现问题,其中全局变量仍然尝试互相使用,而有些变量可能已被销毁。同样,在Main返回之前按正确顺序进行一些显式销毁调用可以使其更清晰。
因此,如果它对你有效,请继续使用,但要注意陷阱。同样的建议适用于C++中几乎所有功能!
你在问题中说你自己认为代码很“棘手”。没有必要为了复杂化事情而过度复杂化。因此,如果你有一个看起来不那么“棘手”的替代方案...那可能更好。

1
我想你指的是静态初始化顺序混乱。谢谢。 - Emadpres

1

C++中不能在任何函数之外添加语句。但是,您可以声明全局对象,并且这些全局对象的构造函数(初始化程序)调用会在main函数开始之前自动进行。在您的示例中,fakeVar是一个全局指针,通过类静态范围的函数进行初始化,这是完全可以的。
甚至一个全局对象也可以提供所需的初始化,只要全局对象的构造函数进行了期望的初始化即可。 例如:

class Fake
{
public:
    Fake()     {
        myMap["test"]=222;
        // Do whatever with your global Variables
    }
};
Fake fake; 

1
这是一个使用统一编译(单翻译单元编译)非常强大的案例。在C和C++编译器中,__COUNTER__宏是事实上的标准,通过它,您可以在全局范围内编写任意命令式代码。
// At the beginning of the file...
template <uint64_t N> void global_function() { global_function<N - 1>(); } // This default-case skips "gaps" in the specializations, in case __COUNTER__ is used for some other purpose.
template <> void global_function<__COUNTER__>() {} // This is the base case.

void run_global_functions();

#define global_n(N, ...) \
template <> void global_function<N>() { \
    global_function<N - 1>(); /* Recurse and call the previous specialization */ \
    __VA_ARGS__; /* Run the user code. */ \
}
#define global(...) global_n(__COUNTER__, __VA_ARGS__)

// ...

std::map<std::string, int> myMap;

global({
    myMap["test"]=222;
    // Do whatever with your global variables
})
global(myMap["Error"] = 111);

int main() {
    run_global_functions();
    std::cout << "Map size: " << myMap.size() << std::endl; // Show myMap has initialized correctly :)
}

global(std::cout << "This will be the last global code run before main!");


// ...At the end of the file

void run_global_functions() {
    global_function<__COUNTER__ - 1>();
}

当你意识到可以使用它来初始化静态变量而不依赖于C运行时,它就变得非常强大。这意味着你可以生成非常小的可执行文件,而无需避免非零全局变量:

// At the beginning of the file...
extern bool has_static_init;
#define default_construct(x) x{}; global(if (!has_static_init()) new (&x) decltype(x){})
// Or if you don't want placement new:
// #define default_construct(x) x{}; global(if (!has_static_init()) x = decltype(x){})

class Complicated {
    int x = 42;
    Complicated() { std::cout << "Constructor!"; }
}
Complicated default_construct(my_complicated_instance); // Will be zero-initialized if the CRT is not linked into the program.

int main() {
    run_global_functions();
}

// ...At the end of the file
static bool get_static_init() {
    volatile bool result = true; // This function can't be inlined, so the CRT *must* run it.
    return result;
}
has_static_init = get_static_init(); // Will stay zero without CRT

1
这个答案与Some programmer dude's answer相似,但可能会被认为更加简洁。自C++17(即添加了std::invoke())以来,您可以像这样做:
#include <functional>

auto initializer = std::invoke([]() {
    // Do initialization here...

    // The following return statement is arbitrary. Without something like it,
    // the auto will resolve to void, which will not compile:
    return true;
});

1
我喜欢你的回答,下次遇到同样的问题我会使用它。我没有将其标记为被接受的答案的唯一原因是它仅限于C++17,而当前被接受的答案更通用。 - Emadpres

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接