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?

Kerrick Staley
  • 1,583
  • 2
  • 20
  • 28
  • 7
    Is there some reason you can't use `std::string`? – scohe001 Jan 30 '19 at 21:59
  • I'm using `mkstemp`, which expects you to pass it a char array (which it mutates). As far as I understand, code like `mkstemp(tmpfile.c_str())` is not allowed because manipulating a `std::string` through its `.c_str()` pointer produces UB. – Kerrick Staley Jan 30 '19 at 22:02
  • 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. – Petr Skocik Jan 30 '19 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. – scohe001 Jan 30 '19 at 22:06
  • @πάντα ῥεῖ This question is different from https://stackoverflow.com/questions/17182588/why-cant-i-assign-an-array-variable-directly-to-another-array-variable-with-the because the arrays here are constant. So there may be some solution here (e.g. using constexpr) that wouldn't be applicable to the other question. – Kerrick Staley Jan 30 '19 at 22:07
  • 1
    @πάντα ῥεῖ is can you please re-open the question? – Kerrick Staley Jan 30 '19 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()`. – πάντα ῥεῖ Jan 30 '19 at 22:15
  • 1
    @πάνταῥεῖ I wasn't aware that std::string::data was an option. In C++14 and earlier, .data() returns a const pointer, and you're not allowed to modify the backing array through it. But in C++17 this is now allowed, which I didn't know. Mind writing up this answer and I'll accept it? If not, I can also write it up. – Kerrick Staley Jan 30 '19 at 22:26
  • 1
    I'm voting to close this question as off-topic because this question shows a lack of understand of the C++ language that is unlikely to be of general interest. – Mikhail Jan 30 '19 at 22:28
  • @KerrickStaley At least `std::string::c_str()` was an option with the first c++ standard introduced. – πάντα ῥεῖ Jan 30 '19 at 22:35
  • @KerrickStaley There are some gotchas with that approach. It will still work well as a null-terminated string (which is what you need), but the reported `length` and `size` can differ from the length of the null-terminated string. In the olden days where there was only `const` bugger access, you could generally use `&str[0]` to get non-`const` access. Same caveats as before. – user4581301 Jan 30 '19 at 22:42
  • @πάνταῥεῖ I need a `char*` (mutable, not `const`). Sorry, I should have made that clearer when asking the question. In C++17 `std::string::data()` will give you a `char*`, in earlier versions it returns a `const char*`. `std::string::c_str()` always returns a `const char*` I believe. – Kerrick Staley Jan 30 '19 at 22:42
  • @PSkocik if you want to type up that answer as an option for C++14 and earlier, I'll upvote it – Kerrick Staley Jan 30 '19 at 22:43

4 Answers4

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
Kerrick Staley
  • 1,583
  • 2
  • 20
  • 28
  • Why would the first approach be C++17 only? Looks like C++11 to me. – HolyBlackCat Jan 30 '19 at 23:15
  • 2
    Because `.data` returns a const pointer in C++14 and earlier. Updated the answer to clarify. – Kerrick Staley Jan 30 '19 at 23:15
  • 2
    `&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`. – asu Jan 30 '19 at 23:22
  • 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 https://stackoverflow.com/questions/1986966/does-s0-point-to-contiguous-characters-in-a-stdstring – Kerrick Staley Jan 30 '19 at 23:37
  • 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. – Kerrick Staley Jan 30 '19 at 23:39
  • As I said in a now-dead question, I've never worked on a platform where `&stringvar[0]` didn't "work". I'm sure they exist. Never used one. "work" is quoted because there are a few caveats. – user4581301 Jan 30 '19 at 23:45
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.

Robert Andrzejuk
  • 5,076
  • 2
  • 22
  • 31
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.

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
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;
    ...
}
Acorn
  • 24,970
  • 5
  • 40
  • 69