6

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?

13
  • 7
    Is there some reason you can't use std::string? Commented Jan 30, 2019 at 21:59
  • 1
    As long as the tmpfile arrays are function-local, you should be able to replace char tmpfile[] = STR_LITERAL; with char tmpfile[sizeof kTmpfile]; memcpy(tmpfile,kTmpfile,sizeof kTmpfile); with no loss of efficiency. Commented Jan 30, 2019 at 22:05
  • 1
    @KerrickStaley then I'd write a nice wrapper for mkstemp that makes things more c++-esque. Something like std::string my_mkstemp(std::string) and then you can do all the ugliness of strcpy'ing into a char array and stuff in one place and reuse it everywhere. Commented Jan 30, 2019 at 22:06
  • 1
    @πάντα ῥεῖ is can you please re-open the question? Commented Jan 30, 2019 at 22:08
  • 1
    @KerrickStaley Reopened. Still not a very good and well researched question though. Just use std:string maybe along with std::string::data() / std::string::c_str(). Commented Jan 30, 2019 at 22:15

4 Answers 4

4

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
Sign up to request clarification or add additional context in comments.

6 Comments

Why would the first approach be C++17 only? Looks like C++11 to me.
Because .data returns a const pointer in C++14 and earlier. Updated the answer to clarify.
&data[0] works an alternative to .data(). Thus the whole thing can be made compatible with C++03: do that and use const char* instead of constexpr auto.
I don't think &data[0] is guaranteed to work in C++03 or earlier, although in practice it will on pretty much any existing compiler. See stackoverflow.com/questions/1986966/…
But I think you might be right, approach 1 can be modified by doing &tmpfile[0] in order to make it compatible with C++11 and later.
|
2

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.

1 Comment

Awesome, thanks for digging into the assembly output and checking that it's the same!
2

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.

Comments

1

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;
    ...
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.