简体   繁体   中英

Initializing char array from constant

I'm starting with this code:

void func1() {
  char tmpfile[] = "/tmp/tmpXXXXXX";
  mkstemp(tmpfile);  // Note: mkstemp modifies the char array, cannot be const
  ...
}

void func2() {
  char tmpfile[] = "/tmp/tmpXXXXXX";
  mkstemp(tmpfile);  // Note: mkstemp modifies the char array, cannot be const
  ...
}

I'd like to refactor this to pull out the shared "/tmp/tmpXXXXXX" constant. Here is an attempt:

constexpr char kTmpfile[] = "/tmp/tmpXXXXXX";

void func1() {
  char tmpfile[] = kTmpfile;
  mkstemp(tmpfile);  // Note: mkstemp modifies the char array, cannot be const
  ...
}

void func2() {
  char tmpfile[] = kTmpfile;
  mkstemp(tmpfile);  // Note: mkstemp modifies the char array, cannot be const
  ...
}

However, this doesn't compile. Changing tmpfile[] to tmpfile[sizeof(kTmpfile)] also doesn't work.

The below does work, but it uses a macro which is discouraged by my company's style guide (which is based on the Google Style Guide ).

#define TMPFILE "/tmp/tmpXXXXXX"

void func1() {
  char tmpfile[] = TMPFILE;
  mkstemp(tmpfile);  // Note: mkstemp modifies the char array, cannot be const
  ...
}

void func2() {
  char tmpfile[] = TMPFILE;
  mkstemp(tmpfile);  // Note: mkstemp modifies the char array, cannot be const
  ...
}

Is there any way to write this "nicely"? Without having to use a macro or hardcode the size? Or is the macro the best option for readability and maintainability?

Here are three approaches. Credit goes to @πάνταῥεῖ, @PSkocik, and @Asu for suggesting these, I just typed them up.

Approach 1a

constexpr auto kTmpfile = "/tmp/tmpXXXXXX";

void func1() {
  std::string tmpfile = kTmpfile;
  mkstemp(tmpfile.data());
  ...
}

Advantages:

  • less code / more readable

Disadvantages:

  • C++17 only, because std::string::data returns a const char* in C++14 and earlier (of course, you can use a const_cast in C++14, but that's also bad)
  • may be slower, since char array may be allocated on the heap instead of stack

Approach 1b

constexpr auto kTmpfile = "/tmp/tmpXXXXXX";

void func1() {
  std::string tmpfile = kTmpfile;
  mkstemp(&tmpfile[0]);
  ...
}

Advantages:

Disadvantages:

  • may be slower, since char array may be allocated on the heap instead of stack

Approach 2

constexpr char kTmpfile[] = "/tmp/tmpXXXXXX";

void func1() {
  char tmpfile[sizeof(kTmpfile)];
  memcpy(tmpfile, kTmpfile, sizeof(kTmpfile));
  mkstemp(tmpfile);
  ...
}

Advantages:

  • only uses stack, no heap
  • compatible with C++14 and earlier

Disadvantages:

  • more verbose / less readable

You could use std::array and some template magic to determine the array size;

#include <array>
#include <algorithm>

constexpr char kTmpfile[] = "/tmp/tmpXXXXXX";

template<typename T, size_t N>
constexpr size_t array_size(T(&)[N])
{
    return N;
}

void func1() {
  std::array<char, array_size(kTmpfile)> var;
  std::copy(std::begin(kTmpfile), std::end(kTmpfile), var.begin());

  mkstemp(var.data());
  //...
}

To get to the data inside a std::array the function data() can be called.

As long as the char arrays are local, you can replace

char tmpfile[] = STR_LITERAL; 

with

char tmpfile[sizeof kTmpfile]; memcpy(tmpfile,kTmpfile,sizeof tmpfile); 

with theoretically no loss of efficiency.

Clang, for example, compiles both func1 and func2 in the snippet below into the same instructions on x86_64:

#include <stdlib.h>

int func1() {
  char tmpfile[] = "/tmp/tmpXXXXXX";
  mkstemp(tmpfile);
}

#include <string.h>
const char kTmpfile[] = "/tmp/tmpXXXXXX";
int func2() {
  char tmpfile[sizeof kTmpfile]; 
  memcpy(&tmpfile,kTmpfile,sizeof tmpfile);

  mkstemp(tmpfile);
}

Assembly output:

func1(): # @func1()
  subq $24, %rsp
  movabsq $24866934413088880, %rax # imm = 0x58585858585870
  movq %rax, 15(%rsp)
  movabsq $8101259051807896623, %rax # imm = 0x706D742F706D742F
  movq %rax, 8(%rsp)
  leaq 8(%rsp), %rdi
  callq mkstemp
func2(): # @func2()
  subq $24, %rsp
  movabsq $24866934413088880, %rax # imm = 0x58585858585870
  movq %rax, 15(%rsp)
  movabsq $8101259051807896623, %rax # imm = 0x706D742F706D742F
  movq %rax, 8(%rsp)
  leaq 8(%rsp), %rdi
  callq mkstemp

This solution for refactoring the duplicate initialization string without using macros also works in plain C.

Using std::string , while it would generally use the heap, which is quite a bit more expensive than the stack, shouldn't hurt here much either as you can expect the file creation to take at least a microsecond and thereby dominate over the heap allocation and string copy.

One more way, C++11 (or C++03 if you write a constructor):

struct KTmpfile { char value[...] = "/tmp/tmpXXXXXX"; };

void func1() {
    KTmpfile tmpfile;
    mkstemp(tmpfile.value);
    ...
}

Stack only, smallest code possible in the func* users.

Note that you have to specify the size of value , of course (or repeat the literal to find out the size).


In my view, this is the best way to do it, because what you really want is a type that you want to get several instances of -- you know its size and its initial value, so create a proper type for it.

And, at this point, since you have a type, you are one step into calling mkstemp from the type itself, maybe even in the constructor, and then simply say:

void func1() {
    KTmpfile tmpfile;
    ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM