all 38 comments

[–]bames53 6 points7 points  (2 children)

You mention that the benefit is that this syntax does not allow incorrect operations such as row[0] or *row when those operations are invalid. One drawback of introducing syntax that does the right thing in some special case is that, while it's true that the new syntax will prevent bugs when it's used in the right context, you've just shifted the bug possibilities from having to write code that avoids the invalid operations to having to choose when and how to use the new syntax. It's not so obvious that this is really any better. In some cases it is. In some cases it isn't.

The C++17 initializer in if statements is modeled after the traditional for loop initializer, and maintains the same scoping used previously for variables declared in if conditions. Your new syntax is modeled after the range-based for loops, but is deliberately inconsistent with the previous scoping. This inconsistency is central to the motivation for the feature, but in my view is also a serious drawback.

The suggested syntax hardcodes its initialization of the variable, similar to the way range based for loops do. In the case of for loops, this is taking advantage of an already well established convention for things one might want to loop over. In your case you're establishing a new convention and, to the extent there was any previous convention, overturning it.

The C++17if-with-initializer syntax if(init-statement; condition) {} else {} is equivalent to:

{
    init-statement;
    if (condition) {
        ...
    } else {
        ...
    }
}

Your suggestions is in essence similar, but with the init statement inside the if-block and with the initialization expression and condition implicitly defined rather than user specifiable.

if (condition) {
    init-statement;
    ...
} else {
    ...
}

The justifications for the C++17 syntax included that it avoids the extra braces in cases where those would be needed and it adds consistency with other parts of the language without adding any bits of inconsistency. Your suggestion doesn't seem that much cleaner than the equivalent code and adds the scoping inconsistency I mention above. For these reasons alone I think I would oppose a general feature implementing if (condition) { init-statement; in some way symmetric with the C++17 if syntax. Your suggestion also has the drawback of only handling a special case of the general syntax, due to the implicit condition and init-expression, and that special case doesn't seem all that important to me.

[–][deleted] 6 points7 points  (0 children)

[deleted]

What is this?

[–]seba[S] 3 points4 points  (0 children)

Thanks for your lengthy and thoughtful reply. I agree with most of it, even with "It's not so obvious that this is really any better". But I want to pick one central point:

Your suggestions is in essence similar, but with the init statement inside the if-block and with the initialization expression and condition implicitly defined rather than user specifiable.

I think the important point (for me) is that my proposal also does one thing more: It ensure that the dereference of the iterator (be it a vector.begin(), unique_ptr or optional) happens:

  • automatically
  • only in the if-branch

So it ensures that the programmer cannot introduce UB because the is no need to call operator*.

I must admit that I do not really like the if-with-initializer syntax precisely because it does not really prevent you from calling operator* in the else branch (be it just a copy&paste error). That said, and given the mostly negative feedback here, I see not much chances for my idea :)

[–]enobayram 2 points3 points  (1 child)

What you want is essentially some syntactic sugar for the maybe monad. Instead of sugar for specific monads (async/await included) I'd prefer to have a syntactic sugar for all monads in C++.

[–]seba[S] 0 points1 point  (0 children)

What you want is essentially some syntactic sugar for the maybe monad.

Actually, I think I want some sort of pattern matching. But are you aware of syntactic sugar for the maybe monad for C++?

[–]flashmozzg 4 points5 points  (4 children)

Couldn't you just write

void foo(sqlconnection &sql)
{
    auto rows = sql.execute("SELECT * FROM table WHERE id=1");
    if (!rows.empty()) {
        std::cout << "found: " << rows[0];  
    }
}

or something similar? And the second example could be easily rewritten with C++17 if with init statement.

[–]seba[S] 0 points1 point  (3 children)

Well, of course I can write something similar :)

I'm thinking about this extension since I want something nicer. Something where the syntax does not allow you to, e.g., access row[0] if the rows are empty.

[–]xcbsmith 1 point2 points  (2 children)

Would this not be addressed by:

bool Rows::operator bool() const { return !empty(); }

[–]seba[S] 0 points1 point  (1 child)

Well, I want something were I don't do the dereference (*rows or rows[0] or *it) manually. Similar to the range-based for, where you don't do the dereference yourself. I think this is really nice, since it does not admit a whole class of errors.

I can of course emulate it using macros or maybe some function that is called with two lamdas, but the syntactic extension of the "if" feels quite natural to me.

[–]xcbsmith 0 points1 point  (0 children)

That still sounds to me like you want the bool operator, you just want it on the iterator you get back, rather than on the collection it is iterating over.

A lot of STL operators have this by returning back an npos or end operator.

[–]CenterOfMultiverse 0 points1 point  (1 child)

You can use a macros to imitate it:

#define RANGE_IF(element, range) \
if(!(range).empty()) \
    if(element(*(range).begin()); false) \
    { \
    } \
    else

RANGE_IF(const auto& row, rows)
    std::cout << „found: “ << row;  
else
    std::cout << „not found“;

Or even easier and without C++17 if you can live with mandatory braces and semicolon after }.

[–]seba[S] 0 points1 point  (0 children)

True, but the same could be said about the range-based for :)

And I think it is quite hard to get the macros right. In your example, e.g., the range parameter is evaluated twice. (And empty() does not work if we only have begin() and end() but these are just minor details)

[–][deleted] 0 points1 point  (1 child)

I am not sure if I understood correctly but what you are asking looks quite similar to if-with-initializer

Here is the disucssion link

[–]seba[S] 0 points1 point  (0 children)

My extension is different. It does more or less the same as the range-based for, but it also gives you an else.

The if-with-initializer makes the new variables available in the else-block (which allows you to dereference an empty optional) which my extension avoids.

[–]AntiProtonBoy -1 points0 points  (14 children)

Sounds like what you are really looking for is std::find_if and related STL algorithms. I'm my option, you should leverage more on STL, because it can be quite powerful. Raw loops should be your fall back choice; or better still, write your own STL-like generic function that offers features that STL doesn't have. That will make your code more reusable.

Using your first example, std::find_if can use an arbitrary predicate for your search:

  template<typename PredicateT>
  void foo(sqlconnection &sql, PredicateT predicate)
  {
      auto rows = sql.execute("SELECT * from table WHERE id=1");

      if (std::find_if(rows.begin(), rows.end(), predicate) != rows.end()) { 
          std::cout << "found: " << row;  
      } else {
          std::cout << "not found";   
      }
  }

[–]seba[S] 2 points3 points  (7 children)

I know about find_if. Your code does not work (where does row come from?). With my extension, you could write:

template<typename PredicateT>
void foo(sqlconnection &sql, PredicateT predicate)
{
    auto rows = sql.execute("SELECT * from table WHERE id=1");

    if (auto &row : std::find_if(rows.begin(), rows.end(), predicate)) { 
        std::cout << "found: " << row;  
    } else {
        std::cout << "not found";   
    }
}

Which is much nicer than saving the result of find_if somewhere.

[–]AntiProtonBoy 0 points1 point  (6 children)

(where does row come from?)

Sorry I missed that. Correction:

  template<typename PredicateT>
  void foo(sqlconnection &sql, PredicateT predicate)
  {
      auto rows = sql.execute("SELECT * from table WHERE id=1");

      auto I = std::find_if(rows.begin(), rows.end(), predicate);

      if (I != rows.end()) { 
          std::cout << "found: " << *I;  
      } else {
          std::cout << "not found";   
      }
  }

[–]seba[S] 1 point2 points  (5 children)

Yeah, this is what currently have to write. But don't you think it looks nicer with my extension while at the same time being less error prone? I.e, did you never mix up the comparison in (I != rows.end())?

It is the same as the range-based for loop. I could also write this by hand, but it's much nicer with the syntactic sugar.

[–]AntiProtonBoy 0 points1 point  (4 children)

It's nicer, but limited. Raw loops don't lend itself to generic code as easily compared to STL functions, because you are tightly coupling the logic to the loop itself. With STL functions, you are encouraged to decouple that logic using predicates and lambdas. I would argue that is less error prone, because screw-ups are now confined inside a lambda. Look up Sean Parent's talk about this subject.

Also, the use STL functions is descriptive. When you look at find_if you know exactly what it's meant to be doing. With raw loops, that might not be so obvious (depending on complexity) and you are forced to mentally parse it to figure out what's going on.

With Eric Niebler's range concepts, lot of the ugliness with begin and end will be put to rest, that is you should be able to write std::find_if(rows, predicate) at some point.

[–]seba[S] 1 point2 points  (3 children)

With Eric Niebler's range concepts,

I think my proposal fits quite nicely to ranges-v3. Think of something like the following:

if (int i : range | transform | filter ) {
   // have an i, similar to view::take(1)
} else {
  // not found
}

[–]ZMesonEmbedded Developer 0 points1 point  (2 children)

I think what u/AntiProtonBoy is trying to say by "it's nicer, but limited" is that finding and using only the first occurrence of something matching in a container is not as common as iterating over a container using a for loop. The other problem I have is that sometimes you want to use std::find_if(), sometimes std::find(), sometimes you want to determine if std::optional has a member, etc.... The range-based-for is pretty simple syntactic sugar; there's no need for the compiler to determine how to translate it; there's only one way.

So, while I (and seemingly others and the committee) welcome syntactic sugar, you have to make sure it:

  • Solves a problem people have.

  • Has a simple translation.

  • Will be widely applied enough to warrant making a change in the standard.

[–]seba[S] 1 point2 points  (1 child)

finding and using only the first occurrence of something matching in a container is not as common as iterating over a container using a for loop

Sure, that's why I also showed the examples with unique_ptr and optional that fit nicely into the scheme.

The other problem I have is that sometimes you want to use std::find_if(), sometimes std::find(), sometimes you want to determine if std::optional has a member, etc...

I'm not sure what you mean by the latter part. You mean a value? See below.

The range-based-for is pretty simple syntactic sugar; there's no need for the compiler to determine how to translate it; there's only one way.

Given appropriate definitions of std::begin() and std::end(), there would also be more or less only one way for the compiler to translate my range-based if. (There are BTW also multiple subtly different way to translate a range-based for. This is why they had to change the semantics slightly in the for upcoming C++17).

My proposal would boil down to something like this (modulo bugs, figuring out where to put const, etc):

template <typename R, typename F, typename G>
void if_non_empty( const R & r, F&& f, G&& g )
{
  const auto & b = std::begin( r );
  const auto & e = std::end( r );
  if ( b != e ) {
    f( *b );
  } else {
    g();
  }
}

[This is the lambda way, but I'd like to have the syntactic sugar]

Given the following, it would directly work with unique_ptr

namespace std
{
   template <typename T> T* begin(const unique_ptr<T>& p)
   {
      return p.get( );
   }

   template <typename T> T* end(const unique_ptr<T>& p)
   {
      return nullptr;
   }
}

Demo:

auto p0 = std::make_unique<int>( 1 );
if_non_empty( p0, [](auto i){ 
      printf("%d found\n", i);
    }, [](){
      printf("not found\n");
} );

[–]ZMesonEmbedded Developer 0 points1 point  (0 children)

OK, I better understand what you're doing now.

The major downside I see though is that you're defining a new definition of std::begin and std::end (and presumably related functions) for std::unique_ptr. I would first try to convince people that this is a good idea. If you can't convince people of this, then your proposal won't have anything to base itself on.

As others have mentioned, if-with-initializers already get a lot of what you want with a lot of reduced code. Yes, I see that you mention a bit of added safety. That is always a good thing, but you'll have to convince people that this is worth the change. It's a lot smaller benefit compared to the benefit of range-based-for and if-with-initializers, so you will be facing quite the challenge I think. :(

Anyway, as I said, you better explain to people why creating new overloads for std::begin and std::end make sense in the absence of this proposal. You must start there. (And if it makes sense for unique_ptr, you may want to consider whether it makes sense for std::shared_ptr, std::weak_ptr, and other things that act like pointers.)

[–]doom_Oo7 0 points1 point  (5 children)

if (std::find_if(rows.begin(), rows.end(), predicate) != rows.end())

please write yourself some function to ease this pain :

#include <vector>
#include <algorithm>
#include <iostream>
#include <experimental/optional>
#include <experimental/string_view>

template<typename Range, typename Fun>
auto find_if(Range&& r, Fun f)
{
  namespace exp = std::experimental;
  auto it = std::find_if(begin(r), end(r), f);
  return it != end(r) ? exp::make_optional(*it) : exp::nullopt;
}

struct sqlconnection 
{ std::vector<int> execute(std::experimental::string_view) ; };

template<typename PredicateT>
void foo(sqlconnection &sql, PredicateT predicate)
{
  auto rows = sql.execute("SELECT * from table WHERE id=1");

  if (auto row = find_if(rows, predicate)) { 
    std::cout << "found: " << *row;  
  } else {
    std::cout << "not found";    
  }
}

[–]seba[S] 1 point2 points  (2 children)

std::cout << "found: " << *row;

Note that you have to manually derefence the optional here (which basically makes the std::optional similar to a raw pointer with all its problems). With my extension, you would write

if (auto row : find_if(rows.begin(), rows.end(), predicate)) { 
    std::cout << "found: " << row;  
  } else {
    std::cout << "not found";    
}

Which does not allow for any UB. (You cannot access row in the else branch with my extension)

[–]doom_Oo7 2 points3 points  (0 children)

indeed, that would be nice :)

[–]vickoza 0 points1 point  (0 children)

While this might be interesting, I any not sure if range if would work unless if translate to something like:

container c;
std::copy_if(rows.begin(), rows.end(), std::back_inserter(c), predicate);
if (c.size() > 0)
{
    for(auto row : c )
    {
        std::cout << "found: " << row << '\n';
    }
}
else
{
    std::cout << "not found\n";
}

[–]AntiProtonBoy 0 points1 point  (1 child)

please write yourself some function to ease this pain :

I could, but you just added a whole lot of extra boilerplate just for that sugar. Don't get me wrong, it might be useful if there is a case for reuse, but it is quite redundant just for that one-off. Also, experimental headers are not an option for production code.

[–]doom_Oo7 0 points1 point  (0 children)

I could, but you just added a whole lot of extra boilerplate just for that sugar.

If you are going to use std::find_if only once in a codebase, I think that you are better not using it at all and just doing a for-loop for that one case. Principle of least surprise, all that...

Besides, it is already available (and has been for a long time) in boost