区分字符串字面值和字符数组

25

我想编写一个函数,它接受字符串字面值——仅仅是字符串字面值

template <size_t N>
void foo(const char (&str)[N]);

很不幸,那太过宽泛了,并且会匹配任何char数组 - 不管它是否是真正的字符串字面量。虽然无法在编译时区分它们之间的差异 - 除非不得不要求调用者封装这个字面量/数组 - 但在运行时,这两个数组将在完全不同的内存位置上:

foo("Hello"); // at 0x400f81

const char msg[] = {'1', '2', '3'};
foo(msg); // at 0x7fff3552767f

有没有办法知道字符串数据可能存在的内存位置,以便我至少可以assert函数只接受字符串字面量? (使用gcc 4.7.3,但实际上任何编译器的解决方案都很好)。


13
即使这是可能的(我强烈怀疑),我也会质疑你需要区分这两种情况的目的的有效性。这个要求听起来相当不寻常。 - Sergey Kalinichenko
5
这并不罕见。字符串字面值的生命周期保证与程序持续时间相等。这是一种非常有用的特性,可以检测到。 - Benjamin Lindley
1
虽然我同意@dasblinkenlight关于动机的观点,但你可以查看字符串的地址与已知字符串字面量的地址、堆栈上的某个东西的地址以及堆上的某个东西的地址。字符串字面量通常存储在靠近可执行代码页面的单独内存位置中,而不是在堆栈或堆上。但是,这取决于编译器。 - iwolf
3
@MattMcNabb:我真的不明白你试图表达什么。字符串字面量的生命周期肯定是已知的,这个事实绝对不是无关紧要的。而且,能够拥有一个不动态分配任何内存、不复制字符串数据、并且可以自由传递而不用担心无效的不可变字符串类肯定是有用的。你也可以从这样的对象中取子字符串,并且它们会有相同的保证。 - Benjamin Lindley
2
@MattMcNabb:任何需要将字符串作为参数并需要存储它(并确保它不会更改)的函数都可以有一个更优化的版本,该版本采用字符串字面值。更优化的原因在于它不分配任何内存,也不复制字符串的任何字符。这可能是一种微观优化,在许多情况下并不需要,但我不明白为什么编译器应该毫无必要地丢弃信息。零开销原则之类的。 - Benjamin Lindley
显示剩余9条评论
3个回答

13

您似乎认为“真实字符串文字”的必要特征是编译器将其嵌入可执行文件的静态存储中。

事实并非如此。C和C++标准保证字符串文字具有静态存储持续时间,因此它必须存在于程序的生命周期内,但是如果编译器可以安排这一点而不将文字放在静态存储中,则可以自由地这样做,有些编译器有时会这样做。

然而,显然你想要测试的属性对于给定的字符串文字是是否实际上在静态存储中。由于根据语言标准,它不需要在静态存储中,因此不能仅仅基于可移植的C/C++解决你的问题。

一个给定字符串字面值实际上是否在静态存储中,是指该字符串字面值的地址是否位于分配给链接节的地址范围之一,并且这些链接节符合所用工具链构建程序时的静态存储术语。

因此我建议的解决方案是使您的程序能够了解那些符合静态存储的链接段的地址范围,然后它就可以通过显然的代码测试给定的字符串文字是否在静态存储中。

这里是一个玩具C++项目prog的示例,使用GNU/Linux x86_64工具链构建(C++98或更高版本可以,对于C而言稍微麻烦一些)。在此设置中,我们以ELF格式链接,并且我们将认为链接节 .bss(0初始化的静态数据),.rodata(只读静态)和 .data(读/写静态数据)是“静态存储”。

以下是我们的源文件:

section_bounds.h

#ifndef SECTION_BOUNDS_H
#define SECTION_BOUNDS_H
// Export delimiting values for our `.bss`, `.rodata` and `.data` sections
extern unsigned long const section_bss_start;
extern unsigned long const section_bss_size;
extern unsigned long const section_bss_end;
extern unsigned long const section_rodata_start;
extern unsigned long const section_rodata_size;
extern unsigned long const section_rodata_end;
extern unsigned long const section_data_start;
extern unsigned long const section_data_size;
extern unsigned long const section_data_end;
#endif
// Assign either placeholder or pre-defined values to 
// the section delimiting globals.
#ifndef BSS_START
#define BSS_START 0x0
#endif
#ifndef BSS_SIZE
#define BSS_SIZE 0xffff
#endif
#ifndef RODATA_START
#define RODATA_START 0x0
#endif
#ifndef RODATA_SIZE
#define RODATA_SIZE 0xffff
#endif
#ifndef DATA_START
#define DATA_START 0x0
#endif
#ifndef DATA_SIZE
#define DATA_SIZE 0xffff
#endif
extern unsigned long const 
    section_bss_start = BSS_START;
extern unsigned long const section_bss_size = BSS_SIZE;
extern unsigned long const 
    section_bss_end = section_bss_start + section_bss_size;
extern unsigned long const 
    section_rodata_start = RODATA_START;
extern unsigned long const 
    section_rodata_size = RODATA_SIZE;
extern unsigned long const 
    section_rodata_end = section_rodata_start + section_rodata_size;
extern unsigned long const 
    section_data_start = DATA_START;
extern unsigned long const 
    section_data_size = DATA_SIZE;
extern unsigned long const 
    section_data_end = section_data_start + section_data_size;

cstr_storage_triage.h

:这是一个文件名,可能与C++字符串存储有关。
#ifndef CSTR_STORAGE_TRIAGE_H
#define CSTR_STORAGE_TRIAGE_H

// Classify the storage type addressed by `s` and print it on `cout`
extern void cstr_storage_triage(const char *s);

#endif

cstr_storage_triage.cpp

#include "cstr_storage_triage.h"
#include "section_bounds.h"
#include <iostream>

using namespace std;

void cstr_storage_triage(const char *s)
{
    unsigned long addr = (unsigned long)s;
    cout << "When s = " << (void*)s << " -> \"" << s << '\"' << endl;
    if (addr >= section_bss_start && addr < section_bss_end) {
        cout << "then s is in static 0-initialized data\n";
    } else if (addr >= section_rodata_start && addr < section_rodata_end) {
        cout << "then s is in static read-only data\n";     
    } else if (addr >= section_data_start && addr < section_data_end){
        cout << "then s is in static read/write data\n";
    } else {
        cout << "then s is on the stack/heap\n";
    }       
}

main.cpp

// Demonstrate storage classification of various arrays of char 

#include "cstr_storage_triage.h"

static char in_bss[1];
static char const * in_rodata = "In static read-only data";
static char in_rwdata[] = "In static read/write data";  

int main()
{
    char on_stack[] = "On stack";
    cstr_storage_triage(in_bss);
    cstr_storage_triage(in_rodata);
    cstr_storage_triage(in_rwdata);
    cstr_storage_triage(on_stack);
    cstr_storage_triage("Where am I?");
    return 0;
}

这是我们的makefile:

.PHONY: all clean

SRCS = main.cpp cstr_storage_triage.cpp section_bounds.cpp 
OBJS = $(SRCS:.cpp=.o)
TARG = prog
MAP_FILE = $(TARG).map

ifdef AGAIN
BSS_BOUNDS := $(shell grep -m 1 '^\.bss ' $(MAP_FILE))
BSS_START := $(word 2,$(BSS_BOUNDS))
BSS_SIZE := $(word 3,$(BSS_BOUNDS))
RODATA_BOUNDS := $(shell grep -m 1 '^\.rodata ' $(MAP_FILE))
RODATA_START := $(word 2,$(RODATA_BOUNDS))
RODATA_SIZE := $(word 3,$(RODATA_BOUNDS))
DATA_BOUNDS := $(shell grep -m 1 '^\.data ' $(MAP_FILE))
DATA_START := $(word 2,$(DATA_BOUNDS))
DATA_SIZE := $(word 3,$(DATA_BOUNDS))
CPPFLAGS += \
    -DBSS_START=$(BSS_START) \
    -DBSS_SIZE=$(BSS_SIZE) \
    -DRODATA_START=$(RODATA_START) \
    -DRODATA_SIZE=$(RODATA_SIZE) \
    -DDATA_START=$(DATA_START) \
    -DDATA_SIZE=$(DATA_SIZE)
endif

all: $(TARG)

clean:
    rm -f $(OBJS) $(MAP_FILE) $(TARG)

ifndef AGAIN
$(MAP_FILE): $(OBJS)
    g++ -o $(TARG) $(CXXFLAGS) -Wl,-Map=$@ $(OBJS) $(LDLIBS)
    touch section_bounds.cpp

$(TARG): $(MAP_FILE)
    $(MAKE) AGAIN=1
else
$(TARG): $(OBJS)
    g++ -o $@ $(CXXFLAGS) $(OBJS) $(LDLIBS)
endif

下面是make的样子:

$ make
g++    -c -o main.o main.cpp
g++    -c -o cstr_storage_triage.o cstr_storage_triage.cpp
g++    -c -o section_bounds.o section_bounds.cpp
g++ -o prog  -Wl,-Map=prog.map main.o cstr_storage_triage.o section_bounds.o 
touch section_bounds.cpp
make AGAIN=1
make[1]: Entering directory `/home/imk/develop/SO/string_lit_only'
g++  -DBSS_START=0x00000000006020c0 -DBSS_SIZE=0x118 -DRODATA_START=0x0000000000400bf0
 -DRODATA_SIZE=0x120 -DDATA_START=0x0000000000602070 -DDATA_SIZE=0x3a
  -c -o section_bounds.o section_bounds.cpp
g++ -o prog  main.o cstr_storage_triage.o section_bounds.o

最后,prog 做什么:

$ ./prog
When s = 0x6021d1 -> ""
then s is in static 0-initialized data
When s = 0x400bf4 -> "In static read-only data"
then s is in static read-only data
When s = 0x602090 -> "In static read/write data"
then s is in static read/write data
When s = 0x7fffa1b053a0 -> "On stack"
then s is on the stack/heap
When s = 0x400c0d -> "Where am I?"
then s is in static read-only data

如果你已经知道它是如何工作的,那么你就不需要继续阅读了。

即使在我们不知道静态存储区域的地址和大小的情况下,程序仍然可以编译和链接。毕竟,这是必须的,不是吗?在这种情况下,应该保存这些值的全局section_*变量都会使用占位符值构建。

当运行make命令时,执行以下操作:

$(TARG): $(MAP_FILE)
    $(MAKE) AGAIN=1

$(MAP_FILE): $(OBJS)
    g++ -o $(TARG) $(CXXFLAGS) -Wl,-Map=$@ $(OBJS) $(LDLIBS)
    touch section_bounds.cpp

由于未定义 AGAIN,这些操作是有效的。它们告诉 make ,为了构建 prog,必须首先按照第二个配方构建 prog 的链接器映射文件,然后重新为 section_bounds.cpp 设置时间戳。在此之后,make 将再次调用自身,并定义 AGAIN = 1。

再次执行带有定义好的 AGAIN 的 makefile,make 现在发现必须计算所有变量:

BSS_BOUNDS
BSS_START
BSS_SIZE
RODATA_BOUNDS
RODATA_START
RODATA_SIZE
DATA_BOUNDS
DATA_START
DATA_SIZE
对于每个静态存储段S,它通过在链接器映射文件中查找报告S地址和大小的行来计算S_BOUNDS。从该行开始,将第二个单词(=部分地址)分配给S_START,将第三个单词(=部分大小)分配给S_SIZE。然后,所有分隔符值都通过-D选项附加到CPPFLAGS上,这些选项将自动传递给编译过程。因为定义了AGAIN,所以$(TARG)的操作性方案现在是惯例:
$(TARG): $(OBJS)
    g++ -o $@ $(CXXFLAGS) $(OBJS) $(LDLIBS)

但是我们在父级make中涉及到了section_bounds.cpp文件;因此它必须重新编译,进而需要重新链接prog。这次编译section_bounds.cpp时,会编译所有的分节定界宏:

BSS_START
BSS_SIZE
RODATA_START
RODATA_SIZE
DATA_START
DATA_SIZE

这些值将具有预定义的值,并且不会假设它们的占位符值。

那些预定义的值是正确的,因为第二个连接没有向链接添加任何符号,也没有删除符号,并且不会更改任何符号的大小或存储类。它只是为在第一个链接中存在的符号分配不同的值。因此,静态存储部分的地址和大小将不会被更改,并且现在已知道它们对于您的程序是什么。


6

根据您的需求,这可能适用或不适用:

#include <cstdlib>

template <size_t N>
void foo(const char (&str)[N]) {}

template <char> struct check_literal {};

#define foo(arg) foo((check_literal<arg[0]>(),arg))    

int main()
{

    // This compiles
    foo("abc");

    // This does not
    static const char abc[] = "abc";
    foo(abc);
}

这仅适用于使用-std=c++11模式的g++和clang++编译器。


这太棒了。 - Quant

1

您可以使用用户定义字面量,这些字面量仅适用于字面量:

#include <iostream>

struct literal_wrapper
{
    const char* const ptr;
private:
    constexpr literal_wrapper(const char* p) : ptr(p) {}
    friend constexpr literal_wrapper operator "" _lw(const char* p, std::size_t);
};
constexpr literal_wrapper operator "" _lw(const char* p, std::size_t){ return literal_wrapper(p); }

literal_wrapper f()
{
    std::cout << "f()" << std::endl;
    return "test"_lw;
}

void foo(const literal_wrapper& lw)
{
    std::cout << "foo:" << lw.ptr << " " << static_cast<const void*>(lw.ptr) << std::endl;
}

int main()
{
    auto x1 = f(), x2 = f(), x3 = f();
    const void* p1 = x1.ptr;
    const void* p2 = x2.ptr;
    const void* p3 = x3.ptr;
    std::cout << x1.ptr << " " << p1 << " " << p2 << " " << p3 << std::endl;

    foo(x1);
    foo(x2);
    foo("test"_lw);
    foo("test2"_lw);
}

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