问题
Interfaces to string classes typically have of method named IsEmpty
(VCL) or empty
(STL). That's absolutely reasonable because it's a special case, but the code that uses these methods often has to negate this predicate, which leads to a "optical (and even psychological) overhead" (the exclamation mark is not very obvious, especially after an opening parenthesis). See for instance this (simplified) code:
/// format an optional time specification for output
std::string fmtTime(const std::string& start, const std::string& end)
{
std::string time;
if (!start.empty() || !end.empty()) {
if (!start.empty() && !end.empty()) {
time = "from "+start+" to "+end;
} else {
if (end.empty()) {
time = "since "+start;
} else {
time = "until "+end;
}
}
}
return time;
}
It has four negations, because the empty cases are those to be skipped. I often observe this kind of negation, also when designing interfaces, and it's not a big problem but it's annoying. I only wish to support writing understandable and easy-to-read code. I hope you'll understand my point.
Maybe I'm only struck with blindness: How would you solve the above problem?
Edit: After reading some comments, I think it's nessessary to say that the original code uses the class System::AnsiString
of the VCL. This class provides an IsEmpty
method, which is very readable:
if (text.IsEmpty()) { /* ... */ } // read: if text is empty ...
if not negated:
if (!text.IsEmpty()) { /* ... */} // read: if not text is empty ...
...instead of if text is not empty. I think the literal is
was better left to the reader's fantasy to let also the negation work well. Ok, maybe not a widespread problem...
回答1:
In most cases you can reverse the order of the if
and the else
to clean up the code:
const std::string fmtTime(const std::string& start, const std::string& end)
{
std::string time;
if (start.empty() && end.empty()) {
return time;
}
if (start.empty() || end.empty()) {
if (end.empty()) {
time = "since "+start;
} else {
time = "until "+end;
}
} else {
time = "from "+start+" to "+end;
}
return time;
}
Or even cleaner after some more refactoring:
std::string fmtTime(const std::string& start, const std::string& end)
{
if (start.empty() && end.empty()) {
return std::string();
}
if (start.empty()) {
return "until "+end;
}
if (end.empty()) {
return "since "+start;
}
return "from "+start+" to "+end;
}
And for the ultimate compactness (although I prefer the previous version, for its readability):
std::string fmtTime(const std::string& start, const std::string& end)
{
return start.empty() && end.empty() ? std::string()
: start.empty() ? "until "+end
: end.empty() ? "since "+start
: "from "+start+" to "+end;
}
Another possibility is to create a helper function:
inline bool non_empty(const std::string &str) {
return !str.empty();
}
if (non_empty(start) || non_empty(end)) {
...
}
回答2:
I think I'd eliminate the conditions in favor of a little math:
const std::string fmtTime(const std::string& start, const std::string& end) {
typedef std::string const &s;
static const std::function<std::string(s, s)> f[] = {
[](s a, s b) { return "from " + a + " to " + b; }
[](s a, s b) { return "since " + a; },
[](s a, s b) { return "until " + b; },
[](s a, s b) { return ""; },
};
return f[start.empty() * 2 + end.empty()](start, end);
}
Edit: if you prefer, you can express the math as start.empty() * 2 + end.empty()
. To understand what's going on, perhaps it's best if I expound on how I thought of things to start with. I thought of things as a 2D array:

(Feel free to swap the "start empty" and "end empty", depending on whether you prefer to think in row-major or column-major order).
The start.empty()
and end.empty()
(or the logical not
of them, if you prefer) each act as as an index along one dimension of this 2D matrix. The math involved simply "linearizes" that addressing, so instead of two rows and two columns, we get one long row, something like this:

In mathematical terms, that's a simple matter of "row * columns + column" (or, again, vice versa, depending on whether you prefer row-major or column-major ordering). I originally expressed the * 2
part as a bit-shift and the addition as a bit-wise or
(knowing the least significant bit is empty, because of the previous left-shift). I find that easy to deal with, but I guess I can understand where others might not.
I should probably add: although I've already mentioned row-major vs. column-major, it should be fairly obvious that the mapping from the two "x.empty" values to positions in the array is basically arbitrary. The value we get from .empty()
means that we get a 0 when the value is not present, and a 1 when it is. As such, a direct mapping from the original values to the array positions is probably like this:

Since we're linearizing the value we have a few choices for how we do the mapping:
- simply arrange the array to suit the values as we get them.
- invert the value for each dimension individually (this is basically what led to the original question--the constant use of
!x.empty()
) - Combine the two inputs into a single linear address, then "invert" by subtracting from 3.
For those who doubt the efficiency of this, it actually compiles down to this (with VC++):
mov eax, ebx
cmp QWORD PTR [rsi+16], rax
sete al
cmp QWORD PTR [rdi+16], 0
sete bl
lea eax, DWORD PTR [rbx+rax*2]
movsxd rcx, eax
shl rcx, 5
add rcx, r14
mov r9, rdi
mov r8, rsi
mov rdx, rbp
call <ridiculously long name>::operator()
Even the one-time construction for f
isn't nearly as bad as some might think. It doesn't involve dynamic allocation, or anything on that order. The names are long enough that it looks a little scary initially, but in the end, it's mostly four repetitions of:
lea rax, OFFSET FLAT:??_7?$_Func_impl@U?$_Callable_obj@V<lambda_f466b26476f0b59760fb8bb0cc43dfaf>@@$0A@@std@@V?$allocator@V?$_Func_class@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBV12@AEBV12@@std@@@2@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@2@AEBV42@AEBV42@@std@@6B@
mov QWORD PTR f$[rsp], rax
Leaving out the static const
doesn't really seem to affect execution speed much. Since the table is static, I think it should be there, but as far as execution speed goes, it's not the kind of massive win we might expect if the table initialization involved four separate dynamic allocations, or anything like that.
回答3:
You could say
if (theString.size()) { .... }
Whether that is more readable is a different matter. Here you are calling a method whose primary purpose is not to tell you if the thing is empty, and relying on an implicit conversion to bool
. I would prefer the !s.empty()
version. I might use not instead for fun:
if (not theString.empty()) { .... }
It might be interesting to see the correlation between people who find the !
and not
versions confusing.
回答4:
I have to refactor this, purely out of anal retentive disorder…
std::string fmtTime( const std::string & start, const std::string & end ) {
if ( start.empty() ) {
if ( end.empty() ) return ""; // should diagnose an error here?
return "until " + end;
}
if ( end.empty() ) return "since " + start;
return "from " + start + " to " + end;
}
There… clean clean clean. If something here is difficult to read, add a comment, not another if
clause.
回答5:
Usually it's just better to not use such complicated conditional code. Why not keep it simple?
const std::string fmtTime(const std::string& start, const std::string& end)
{
if (start.empty() && end.empty())
{
return "";
}
// either start or end or both are not empty here.
std::string time;
if (start.empty())
{
time = "until "+end;
}
else if (end.empty())
{
time = "since "+start;
}
else // both are not empty
{
time = "from "+start+" to "+end;
}
return time;
}
回答6:
Globally, I have no problem with the way you've written it; it's
certainly cleaner that the alternatives that others are
proposing. If you're worried about the !
disappearing (which
is a legitimate worry), use more white space.
if ( ! start.empty() || ! end.empty() ) ...
Or try using the keyword not
instead:
if ( not start.empty() || not end.empty() ) ...
(With most editors, the not
will be highlighted as a keyword,
which will draw even more attention to it.)
Otherwise, two helper functions:
template <typename Container>
bool
isEmpty( Container const& container )
{
return container.empty();
}
template <typename Container>
bool
isNotEmpty( Container const& container )
{
return !container.empty();
}
This has the added advantage of giving the functionality
a better name. (Function names are verbs, so c.empty()
logically means "empty the container", and not "is the container
empty". But if you start wrapping all of the functions in the
standard library that have poor names, you've got your work cut
out for you.)
回答7:
Without using negation.. ;)
const std::string fmtTime(const std::string& start, const std::string& end)
{
std::string ret;
if (start.empty() == end.empty())
{
ret = (start.empty()) ? "" : "from "+start+" to "+end;
}
else
{
ret = (start.empty()) ? "until "+end : "since "+start;
}
return ret;
}
EDIT: okay cleaned up a little more...
回答8:
Since no one cared to type the complete answer with my comment, here it goes:
Create local variables that simplify the reading of expressions:
std::string fmtTime(const std::string& start, const std::string& end)
{
std::string time;
const bool hasStart = !start.empty();
const bool hasEnd = !end.empty();
if (hasStart || hasEnd) {
if (hasStart && hasEnd) {
time = "from "+start+" to "+end;
} else {
if (hasStart) {
time = "since "+start;
} else {
time = "until "+end;
}
}
}
return time;
}
The compiler is smart enough to elide those variables, and even if it did not, it won't be less efficient than the original (I expect both to be a single test of a variable). The code now is a bit more readable for a human that can just read the conditions:
if has start or end then
Of course you might also do different refactors to further simplify the number of nested operations, like singling out when there is no start or end and bailing out early...
回答9:
I struggle with the psychological overhead of negative logic as well.
One solution to this (when it cannot be avoided) is to check for the explicit condition, consider:
if (!container.empty())
vs
if (container.empty() == false)
The second version is easier to read because it flows as you would read it out loud. It also makes it clear that you're checking a false condition.
Now if that is still not good enough for you, my advice would be to create a thin wrapper class that inherits from whatever container you're using and then create your own method for that particular check.
For example with strings:
class MyString : public std::string
{
public:
bool NotEmpty(void)
{
return (empty() == false);
}
};
Now it becomes just:
if (container.NotEmpty())...
回答10:
If all you're concerned about is the ease with which !
can be overlooked, you can use the standard C++ alternative token not
instead:
const std::string fmtTime(const std::string& start, const std::string& end)
{
std::string time;
if (not start.empty() or not end.empty()) {
if (not start.empty() and not end.empty()) {
time = "from "+start+" to "+end;
} else {
if (end.empty()) {
time = "since "+start;
} else {
time = "until "+end;
}
}
}
return time;
}
(Refer to [lex.digraph]
in the standard for alternative tokens)
回答11:
Would you consider assigned a good opposite?
#include <string>
template <typename CharType>
bool assigned(const std::basic_string<CharType>& s)
{
return !s.empty();
}
std::string fmtTimeSpec(const std::string& from, const std::string& to)
{
if (assigned(from)) {
if (assigned(to)) {
return "from "+from+" to "+to;
}
return "since "+from;
}
if (assigned(to)) {
return "until "+to;
}
return std::string();
}
Structural improvements of the "test function" came from numerous useful answers. Special thanks to:
- C. E. Gesser
- Potatoswatter
来源:https://stackoverflow.com/questions/21882655/is-there-a-concise-opposite-of-is-empty