سؤال

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

هل كانت مفيدة؟

المحلول

In most cases you can reverse the order of the ifand 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)) {
...
}

نصائح أخرى

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:

enter image description here

(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:

enter image description here

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:

enter image description here

Since we're linearizing the value we have a few choices for how we do the mapping:

  1. simply arrange the array to suit the values as we get them.
  2. invert the value for each dimension individually (this is basically what led to the original question--the constant use of !x.empty())
  3. 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.

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.

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.

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

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.)

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

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

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())...

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)

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:

To express the opposite form of ".isEmpty()" usage, I prefer this way:

 if (textView.getText().toString().isEmpty()){

            //do the thing if textView has nothing inside as typed.

 }else if (textView.getText().toString() != ""){

            // do the thing if textView has something inside as typed.
        }

Also, you may use ".equals("")" instead of "!=" typography as recommended by Android Studio.

textView.getText().toString().equals("")

Coming back to the API design aspect

(it may not be applicable to strings, but on container classes in general)

By pure chance I found an excellent answer to this old question (emphasizes mine)

What about using any()? [...]

in a completely unrelated post being the answer to the question

How do I know if a generator is empty from the start?

To contrast empty and any might be poor in English but it absolutely makes sense in API design.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top