boost::program_options positional options

The run() member function gives you back an instance of type parsed_options. The simple usage is to never actually look at this object and pass it directly into store(), as in your example:

po::store(
    po::command_line_parser(argc, argv).options(opts_desc).positional(pos_opts_desc).run(),
    opts_var_map);

But we can hold onto it and examine its contents:

auto parsed = po::command_line_parser(argc, argv)
    .options(opts_desc)
    .positional(pos_opts_desc)
    .run();
po::store(parsed, opts_var_map);

The parsed_options class has a member options which has an ordered list of all the options (unlike the variable map, which is ordered by option name - since it's a std::map). So you can look up the "filename" argument and check its position_key member. We want either: position_key == -1 (which means it provided with -F) or position_key == 0 and it being the last element in the options list (it was a positional argument that was the last argument):

auto it = std::find_if(parsed.options.begin(),
                       parsed.options.end(),
                       [](po::option const& o) {
                         return o.string_key == "filename";
                       });

if (it == parsed.options.end()) {
    // fail: missing filename);
}

if (it->position_key != -1 && it != std::prev(parsed.options.end())) {
    // fail: filename was positional but wasn't last
}

variables_map is as the name suggests a std::map, which allows us to use regular STL functions on it.

    if ( vm.count("filename") ) {
      if ( vm.find("filename") != std::prev(vm.rbegin()).base() ) {
        std::cout << "filename must go at the end.";
      }
    }

Test cases:

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp -lboost_system -lboost_program_options \
&& echo -n "Case 1 " && ./a.out asdf --foo=12 && echo \
&& echo -n "Case 2 " && ./a.out --foo=12 asdf && echo \
&& echo -n "Case 3 " && ./a.out asdf && echo \
&& echo -n "Case 4 " && ./a.out --foo=12 && echo \
&& echo -n "Case 5 " && ./a.out && echo \
&& echo -n "Case 6 " && ./a.out --foo=12 asdf asdf

Result:

Case 1 filename must go at the end.
Case 2 
Case 3 
Case 4 
Case 5 
Case 6 option '--filename' cannot be specified more than once